LifeSports Application(ReactNative & Nest.js) - 19. 마이페이지 대관 내역

#1 UI

UI를 수정해보도록 하겠습니다.

  • ./src/pages/user/components/MyRentalFragment.js
import React, { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { getRentals } from '../../../modules/rentals';
import Loading from '../../../styles/common/Loading';
import palette from '../../../styles/palette';
import MyRenalCard from './MyRentalCard';

const MyRentalFramgment = () => {
    const dispatch = useDispatch();
    const { 
        userId,
        rentals
    } = useSelector(({ 
        user,
        rentals
    }) => ({
        userId: user.user.userId, 
        rentals: rentals.rentals 
    }));
    
    useEffect(() => {
        dispatch(getRentals(userId));
    }, [dispatch]);

    return(
        <View style={ styles.container }>
            {
                rentals ?
                rentals.map((item, i) => {
                    return <MyRenalCard i={ i }
                                        item={ item }
                            />
                }) : <Loading />
            }
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: palette.gray[2],
    }
});

export default MyRentalFramgment;
  • ./src/pages/user/components/MyRentalCard.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import PaymentButton from '../components/PaymentButton';
import palette from '../../../styles/palette';

const MyRenalCard = ({ item }) => {
    return(
        <View style={ styles.container }>
            <Text style={ styles.font }>
                { item.date + " " + item.time }
            </Text>
            <Text style={ styles.font }>
                { item.mapName }
            </Text>
            {
                !item.payment &&
                <PaymentButton rentalId={ item.rentalId }/>
            }
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        alignItems: 'center',
        justifyContent: 'center',
        width: 350,
        marginLeft: 30,
        marginTop: 15,
        marginBottom: 5,
        paddingTop: 10,
        paddingBottom: 10,
        backgroundColor: palette.white[0]
    },
    font: {
        fontWeight: 'bold',
        fontSize: 15
    }
});

export default MyRenalCard;
  • ./src/pages/user/components/PaymentButton.js
import React from 'react';
import { useNavigation } from '@react-navigation/native';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import palette from '../../../styles/palette';
import { useDispatch } from 'react-redux';
import { deleteRental } from '../../../modules/rental';

const PaymentButton = rentalId => {
    const dispatch = useDispatch();
    const navigation = useNavigation();
    const toPaymentPage = e => {
        navigation.navigate("Payment", {
            rentalId: rentalId.rentalId
        });
    };
    const onCancel = e => {
        dispatch(deleteRental(rentalId.rentalId));
    };

    return (
        <View style={ styles.container }>
            <TouchableOpacity style={ styles.payment_shape }
                              onPress={ toPaymentPage } 
            >
                <Text style={ styles.font }>
                    결제하기
                </Text>
           </TouchableOpacity>
           <TouchableOpacity style={ styles.cancel_shape }
                             onPress={ onCancel } 
            >
                <Text style={ styles.font }>
                    취소하기
                </Text>
           </TouchableOpacity>
        </View>
    )
};

const styles = StyleSheet.create({
    container: {
        flexDirection: 'row',
    },
    payment_shape: {
        width: '30%',
        height: 60,
        borderRadius: 6,
        margin: 10,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: palette.blue[2],
    },
    cancel_shape: {
        width: '30%',
        height: 60,
        borderRadius: 6,
        margin: 10,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: palette.red[0],
    },
    font: {
        fontSize: 18,
        color: palette.white[0]
    }
});

export default PaymentButton;

다음과 같이 StackNavigation에서 PaymentScreen을 등록하여 결제하기 버튼을 누르면 결제창으로 넘어갈 수 있도록 수정하도록 하겠습니다.

  • ./src/navigator/user/MyPageStackNavigation.js
...

const Stack = createStackNavigator();

const MyPageStackNavigation = () => {
    return(
        <Stack.Navigator>
           ...
            <Stack.Screen name="Payment"
                          component={ PaymentScreen }
            />
        </Stack.Navigator>
    );
};

export default MyPageStackNavigation;

여기까지 결제내역 스크린 테스트를 진행해보도록 하겠습니다.

대여 내역들이 잘 출력되는 모습을 볼 수 있습니다. 그러면 이어서 결제하기의 클릭이벤트, 취소하기의 클릭이벤트를 위주로 UI를 작성해보도록 하겠습니다.

우선 결제하기입니다. 앞서 작성한 PaymentButton에서 toPaymentPage 이벤트를 보면 rentalId를 navigation에 담아 Payment페이지로 이동할 수 있도록 작성하였습니다. 그러면 PaymentScreen에서 이 rentalId를 받아 대관 상세내역을 볼 수 있겠죠.

  • ./src/pages/payment/components/PaymentFragment.js
import React, { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import palette from '../../../styles/palette';
import Loading from '../../../styles/common/Loading';
import PaymentContent from './PaymentContent';
import { useRoute } from '@react-navigation/native';
import { getRental } from '../../../modules/rentals';

const PaymentFragment = () => {
    const dispatch = useDispatch();
    const route = useRoute();
    const { rental } = useSelector(({ rentals }) => ({ rental: rentals.rental }));

    useEffect(() => {
        dispatch(getRental(route.params.rentalId));
    }, [dispatch, route]);

    return(
        <View style={ styles.container }>
            { 
                rental ? 
                <PaymentContent rental={ rental }/> : 
                <Loading />
            }
        </View> 
    );
};

const styles = StyleSheet.create({
    container: {
        flexDirection: 'column',
        width: '90%',
        height: '90%',
        backgroundColor: palette.white[0],
    },
});

export default PaymentFragment;

rentalId를 기반으로 호출한 대관 데이터를 PaymentContent 컴포넌트로 보내도록 하겠습니다.

  • ./src/pages/payment/components/PaymentContent.js
import React, { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import palette from '../../../styles/palette';
import StyledTextInput from '../../../styles/common/StyledTextInput';
import { initialize } from '../../../modules/rentals';
import { changeField, requestPayment } from '../../../modules/payment';

const PaymentContent = rental => {
    const { 
        paymentName,
        payer,
        rentalId,
        price,
        payment,
        paymentError, 
    } = useSelector(({ 
        payment,
    }) => ({ 
        paymentName: payment.paymentName,
        payer: payment.payer,
        rentalId: payment.rentalId,
        price: payment.price,
        payment: payment.payment,
        paymentError: payment.paymentError,
    }));
    const dispatch = useDispatch();
    const navigation = useNavigation();
    const onPayment = () => {
        dispatch(requestPayment({
            paymentName,
            payer,
            rentalId,
            price
        }));

        dispatch(initialize());
    };

    useEffect(() => {
        dispatch(changeField({
            key: 'paymentName',
            value: rental.rental.mapName
        }))
    }, []);

    useEffect(() => {
        dispatch(changeField({
            key: 'payer',
            value: rental.rental.borrower
        }))
    }, []);

    useEffect(() => {
        dispatch(changeField({
            key: 'rentalId',
            value: rental.rental.rentalId
        }))
    }, []);

    useEffect(() => {
        dispatch(changeField({
            key: 'price',
            value: rental.rental.price
        }))
    }, []);

    useEffect(() => {
        if(payment) {
            dispatch(initialize())

            navigation.navigate("MyPage");
        }

        if(paymentError) {
            // setError
        }
    }, [dispatch, payment, paymentError]);

    return(
        <View>
            <View style={ styles.row } >
                <Text style={ styles.label } >
                    대관 번호
                </Text>
                <StyledTextInput placeholderTextColor={ palette.black[0] }
                                 value={ rental.rental.rentalId }
                />
            </View>
            <View style={ styles.row }>
                <Text style={ styles.label }>
                    결제 내역
                </Text>
                <StyledTextInput placeholderTextColor={ palette.gray[3] }
                                 value={ rental.rental.mapName }
                />
            </View>
            <View style={ styles.row }>
                <Text style={ styles.label }>
                    결제 금액
                </Text>
                <StyledTextInput placeholderTextColor={ palette.gray[3] }
                                 value={ rental.rental.price.toString() }
                />
            </View>
            <View style={ styles.row }>
                <Text style={ styles.label } >
                    사용자명
                </Text>
                <StyledTextInput placeholderTextColor={ palette.gray[3] }
                                 value={ rental.rental.borrower }
                />
            </View>
            <View style={ styles.row } >
                <Text style={ styles.label }>
                    체육관 전화번호
                </Text>
                <StyledTextInput placeholderTextColor={ palette.gray[3] }
                                 value={ rental.rental.tel }
                />
            </View>
            <View style={ styles.row } >
                <Text style={ styles.label } >
                    대관 날짜
                </Text>
                <StyledTextInput placeholderTextColor={ palette.gray[3] }
                                 value={ rental.rental.date + "\t" + rental.rental.time }
                /> 
            </View>
            <View style={ styles.button_container }>
                <TouchableOpacity style={ styles.shape }
                                  onPress={ onPayment }
                >
                    <Text style={ styles.font }>
                        결제하기
                    </Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    row: {
        flexDirection: 'row',
        alignItems: 'center',
        overflow: 'hidden'
    },
    label: {
        fontWeight: 'bold',
        padding: 10,
    },
    button_container: {
        justifyContent: 'center',
        alignItems: 'center',
        marginTop: 30
    },
    shape: {
        justifyContent: 'center',
        alignItems: 'center',
        width: '80%',
        height: 70,
        backgroundColor: palette.blue[2]
    },
    font: {
        justifyContent: 'center',
        alignItems: 'center',
        fontWeight: 'bold',
        fontSize: 20,
        color: palette.white[0]
    }
});

export default PaymentContent;

결제 상세내역 UI를 확인해보겠습니다.

상세내역 UI도 잘 출력이 되는 모습을 볼 수 있습니다.

그러면 취소하기 관련 이벤트도 작성해보도록 하겠습니다.

#2 취소하기

  • ./src/lib/api/rental.js
import client from './client';

...

export const deleteRental = rentalId => client.delete(`http://10.0.2.2:8000/rental-service/${rentalId}/rental`);

rental.js에 deleteRental 액션을 추가하도록 하겠습니다. 이 액션 함수를 이용하여 취소하기 버튼을 누르면 대관 데이터 삭제가 이루어집니다.

  • ./src/modules/rental.js
...

const [
    DELETE_RENTAL,
    DELETE_RENTAL_SUCCESS,
    DELETE_RENTAL_FAILURE
] = createRequestActionTypes('rental/DELETE_RENTAL');

...
export const deleteRental = createAction(DELETE_RENTAL, rentalId => rentalId);

const makeRentalSaga = createRequestSaga(MAKE_RENTAL, rentalAPI.rental);
const deleteRentalSaga = createRequestSaga(DELETE_RENTAL, rentalAPI.deleteRental);

export function* rentalSaga() {
    yield takeLatest(MAKE_RENTAL, makeRentalSaga);
    yield takeLatest(DELETE_RENTAL, deleteRentalSaga);
}

const initialState = {
    price: null,
    borrower: '',
    tel: '',
    userId: '',
    date: null,
    time: null,
    mapId: '',
    mapName: '',
    rental: null,
    rentalError: null,
    message: null,
};  

const rental = handleActions(
    {
        ...
        [DELETE_RENTAL_SUCCESS]: (state, { payload: message }) => ({
            ...state,
            message,
        }),
        [DELETE_RENTAL_FAILURE]: (state, { payload: rentalError }) => ({
            ...state,
            rentalError,
        }),
    },
    initialState,
);

export default rental;

rental-service의 컨트롤러에 endpoint를 추가하겠습니다.

  • ./src/app.controller.ts
...

@Controller('rental-service')
export class AppController {
    ...

    @Delete(':rentalId/rental')
    public async deleteRental(@Param('rentalId') rentalId: string): Promise<any> {
        try {
            const result: any = this.rentalService.deleteRental(rentalId);

            if(result.status === statusConstants.ERROR) {
                return await Object.assign({
                    status: HttpStatus.INTERNAL_SERVER_ERROR,
                    payload: null,
                    message: "Error message: " + result.message
                });
            }

            return await Object.assign({
                status: HttpStatus.OK,
                payload: null,
                message: "Successful delete one"
            });
        } catch(err) {
            return await Object.assign({
                status: HttpStatus.BAD_REQUEST,
                payload: null,
                message: "Error message: " + err
            });
        }
    }

    @EventPattern('PAYMENT_RESPONSE')
    public async responsePayment(data: any): Promise<any> {
        try {
            if(data === 'FAILURE_PAYMENT') {
                const result: any = await this.rentalService.deleteRental(data.rentalId);

                if(result.status === statusConstants.ERROR) {
                    return await Object.assign({
                        status: HttpStatus.INTERNAL_SERVER_ERROR,
                        payload: null,
                        message: "Error message: " + result.message
                    });
                }

                return await Object.assign({
                    status: HttpStatus.INTERNAL_SERVER_ERROR,
                    payload: null,
                    message: "Error message: " + data
                });
            }

            const result: any = await this.rentalService.completeRental(Builder(RentalDto).rentalId(data.rentalId)
                                                                                          .build());

            if(result.status === statusConstants.ERROR) {
                return await Object.assign({
                    status: HttpStatus.INTERNAL_SERVER_ERROR,
                    payload: null,
                    message: "Error message: " + result.message
                });
            }

            return await Object.assign({
               status: HttpStatus.OK,
               payload: null,
               message: "Successful complete rental!" 
            });
        } catch(err) {
            return await Object.assign({
                status: HttpStatus.INTERNAL_SERVER_ERROR,
                payload: null,
                message: "Error message: " + err
            });
        }
    }
}

이전에 RentalService에서 만들었던 deleteOne 메서드의 인자를 string형으로 고치도록 하겠습니다.

  • ./src/rental/rental.service.ts
...

@Injectable()
export class RentalService {
    ...

    public async deleteRental(rentalId: string): Promise<any> {
        try {
            const result  = await this.rentalModel.deleteOne({ rentalId: rentalId });
            
            if(!result) {
                return await Object.assign({
                    statusCode: statusConstants.ERROR,
                    payload: null,
                    message: "Not exist data",
                });
            }

            return await Object.assign({
                statusCode: statusConstants.SUCCESS,
                payload: null,
                message: "Success delete"
            });
        } catch(err) {
            return await Object.assign({
                statusCode: statusConstants.ERROR,
                payload: null,
                message: "rental-service database: " + err,
            });
        }
    }

    ...
}

취소하기 관련 이벤트도 완성이 되었습니다. 그러면 최종 테스트를 진행해보도록 하겠습니다.

#3 테스트

결제하기 버튼을 누르고 다시 대관 내역을 들어가면 다음과 같은 결과를 볼 수 있습니다.

이어서 취소하기 버튼도 테스트 해보도록 하겠습니다.

2번째 데이터를 테스트하겠습니다.

데이터가 정상적으로 사라진 모습을 볼 수 있습니다.

이렇게 payment-service, rental-service 연동을 끝마쳤습니다. 다음 포스트에서는 게시글 관련 포스트를 작성하도록 하겠습니다.

좋은 웹페이지 즐겨찾기