서버 정리4

뜨루루루·2024년 1월 14일

sirenorder

목록 보기
9/9

소켓

오늘은 마지막인 소켓부분을 정리해보겠다.

소켓은 유저,상인,서버 세 가지 모두 관여하는 곳으로
주로 주문 전달, 주문 상태, 주문 등록, 주문 삭제 등등을 담당한다.

우선 상인이 상점에 연결하기 위한 단계는 다음과 같다.

1. 서버와 소켓연결시도
	(실패 시) 연결에 실패한 이유와 상태코드를 반환
2. 소켓으로 로그인 이벤트를 emit
3. 보내온 데이터로 상인의 데이터를 찾음
	(실패 시) 데이터를 찾기 실패 한 이유와 상태코드를 반환
4. 보내온 비밀번호와 찾은 상인의 비밀번호를 대조
	(실패 시) 대조에 실패 한 이유와 상태코드를 반환
5. 상점의 상태를 업데이트 하고 상점의 정보를 반환

(모든 실패에 경우 소켓과의 연결을 끊고 캐시정보 업데이트)

이와 같은 로그인 절차를 마치게 되면 상점 측 에서 주문과 관련된 이벤트인 accept, refuse, finish 를 모두 리슨하게 된다.

상점에 접근하는 방법으로는 상점아이디, 소켓아이디 이 두가지로 접근이 가능하며 주로 유저측은 주문시 상점아이디를 담아 주문할 매장을 선택하게 되는데 이는 상점이 가진 고유번호이며 고유번호 임에도 노출시켜 유저측에서 해당 번호로 접근을 하게한 이유는 해당 상점 아이디로는 상점의 주요한 정보들은 접근할 수 없기 때문이다.

(상인은 주요정보를 접근할때 상인의 고유번호, 접근할 주요정보의 고유번호를 통해 직접적으로 접근하게 된다.)

주문전달

리슨하게 되는 주문 이벤트들을 보자하면 위에 적은 것과 같이 accept, refuse, finish 이 세 가지로 나뉘는데 차근차근 하나씩 살펴보도록 하자.

우선 주문과 관련된 함수는 PortOneMethod 네임 스페이스로 묶어 관리되고 있으며 테스트 결제라 사실상 API 접근은 하지 않는다.

accept 부터 살펴보자

@SubscribeMessage('accept-order')
    async handleAccpetOrder(
        @MessageBody() data: string
    ) : Promise<SocketResponse<
    any,
    | typeof ERROR.ServerDatabaseError
    | typeof ERROR.ServerCacheError
    | typeof ERROR.NotFoundData>> {
        try {
            const buyer_email = await PortOneMethod.acceptOrder({
                order_uid: data,
                redis: this.redis,
                repository: this.storeRepository,
            })
            this.pushStateMessage(buyer_email, "accept")
            return {
                result: true,
                message: "accept",
            }
        } catch(e) {
            await PortOneMethod.removeOrderById({
                redis: this.redis,
                order_uid: data,
                repository: this.storeRepository,
            })
            return {
                result: false,
                message: "fail",
                data: e,
            }
        }
    }

주문수락 이벤트를 핸들링하는 함수의 전체 코드이며, 주문정보를 찾아 주문한 사람의 정보를 찾고 결과 메시지를 푸시 해준다.

포트원의 주문수락 루틴을 보자하면

export const acceptOrder = async ({
        order_uid,
        redis,
        repository,
    }: {
        order_uid: string,
        redis: RedisService,
        repository: StoreRepository,
    }) : Promise<string> => {
        const order = await findOrderByUUID({
            order_uid,
            redis,
        })
        await repository.createOrder(order)
        return order.buyer_email
    }

주문번호를 통해 주문을 찾아오고, 직접적으로 DB에 주문정보가 등록을 시켜준다.
(그 전에는 주문이 들어오면 캐시에만 등록해놓고 DB에는 등록하지 않는다.)

그리고 removeOrderById라는 함수가 있는데 본래는 refuseOrder라는 함수로 포트원 API측에서 주문처리를 같이 해주는 루틴이 있는데 테스트 결제라 주문이 등록되질 않아 removeOrderById로 대신해 주문삭제를 진행하게 된다.

아래는 removeOrderById의 코드이다.

export const removeOrderById = async ({
        redis,
        order_uid,
        repository,
    } : {
        redis: RedisService,
        order_uid: string,
        repository: StoreRepository,
    }) : Promise<RegisteredOrder> => {
        const order = await findOrderByUUID({
            order_uid,
            redis,
        })
        await repository.deleteOrder(order_uid, order.sales_uid)
        redis.delete(order_uid, logPath)
        return order
    }

주문정보를 DB에서도 제거 해주어야 하기 때문에 주문정보를 캐시 데이터에서 먼저 찾고, DB와 캐시 두 곳에서 주문정보를 삭제하고 삭제된 주문정보를 반환해준다.

이번에는 기존에 사용하려 했던 주문취소 루틴을 한번봐 보자

export const refuseOrder = async ({
        redis,
        imp_uid,
        reason,
    }: {
        redis: RedisService,
        imp_uid: string,
        reason: string, 
    }) : Promise<boolean> => {
        const token = await _getCertifiToken()
        const paymentData = await _getPaymentData(imp_uid, token)

        if(paymentData.cancel_amount <= 0) {
            return false // 취소가능 금액이 0원 이하일 경우 처리
        }

        const result = await fetch("https://api.iamport.kr/payments/cancel", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": token,
            },
            body: JSON.stringify({
                reason, 
                imp_uid: paymentData.imp_uid, 
                amount: paymentData.cancel_amount, 
                checksum: paymentData.amount
            })
        })
        .then(res => res.json())
        .then(async json => {
            const { response } = json
            const order_uid = response.merchant_uid

            if(!order_uid) throw ERROR.Accepted
            await redis.delete(order_uid, logPath)
            return true
        })
        .catch(_ => {
            var err = ERROR.BadRequest
            err.substatus = "ForgeryData"
            throw err
        })

        return result
    }

우선 포트원에서 API를 이용하게 위해 권한을 인증받아야 하는데 해당 권한은 토큰으로 발급받아 처리하기 때문에 최초 토큰을 API를 통해 지급받고 진행해야한다.

즉 해당 코드엔 API요청만 3개가 들어가 있는 것이다.

근데 지금보니 토큰은 유효기간이 있으니 전역 변수로 해놓고 유효기간만 체크해서 만료되었을 경우에만 제 발급 받으면 더 좋겠다는 생각이 든다.

아 그리고 여기서 헷갈릴 만한 부분이 order_uid를 res에서 merchant_uid로 받아오는데 이는 랜덤한 주문번호를 포트원에서 저 이름의 인자로 뿌려주기 때문에 기존에 상인의 고유번호와 헷갈리지 않도록 유의해야한다.

다음은 주문취소를 보자

@SubscribeMessage('refuse-order')
    async handleRefuseOrder(
        @MessageBody() data: RefuseOrder
    ) : Promise<SocketResponse<
    any, 
    | typeof ERROR.ServerDatabaseError
    | typeof ERROR.ServerCacheError
    | typeof ERROR.NotFoundData
    >> {
        try {
            const { buyer_email } = await PortOneMethod.removeOrderById({
                redis: this.redis,
                order_uid: data.uuid,
                repository: this.storeRepository,
            })
            this.pushStateMessage(buyer_email, "refuse")
            return {
                result: true,
                message: "refuse",
            }
        } catch(e) {
            return {
                result: false,
                message: "fail",
                data: e,
            }
        }
    }

위에서 설명한 removeOrderById로 주문취소를 진행하고, 주문정보에서 주문자의 정보를 찾아 주문 상태를 전달해주고 상인에게 처리결과를 반환해준다.

지금보니 오류가 발생했을 경우, 유저에게도 오류가 발생해 주문취소에 실패했음을 알리는 루틴이 하나 더 있어야 할거같다.

마지막으로 주문 완료루틴 해당 루틴은 매장에서 주문을 받고, 받은 음식의 준비가 모두 끝났고 손님에게 전달만 하면 된다는 신호로 서버 측 에서는 해당 이벤트 호출 시 거래가 정상적으로 끝났다고 판단하게 된다.

코드를 한번봐 보자.

@SubscribeMessage('finish-order')
    async handleFinishOrder(
        @MessageBody() data: string
    ) : Promise<SocketResponse<
    any,
    | typeof ERROR.ServerDatabaseError
    | typeof ERROR.ServerCacheError
    | typeof ERROR.NotFoundData>> {
        try {
            const { buyer_email, totalprice, history } = await PortOneMethod.finishOrder({
                order_uid: data,
                redis: this.redis,
                service: this.userService,
                repository: this.storeRepository,
            })
            
            this.pushStateMessage(
                buyer_email, 
                "finish",
                {
                    increase_point: Math.max(1, (totalprice as number) / 1000),
                    increase_stars: 1,
                    history,
                }
            )
            return {
                result: true,
                message:"finish",
            }
        } catch(e) {
            return {
                result: false,
                message: "fail",
                data: e,
            }
        }
    }

주문준비를 완료한 주문의 정보를 받아와 손님에게 주문준비가 끝났음을 알리고 앱 데이터와 서버데이터의 동기화를 위해 동기화에 필요한 데이터를 메세지와 함께 보내준다.

그럼 포트원쪽 주문준비 완료 함수를 보자.

export const finishOrder = async ({
        order_uid,
        redis,
        service,
        repository,
    }: {
        order_uid: string,
        redis: RedisService,
        service: UserService,
        repository: StoreRepository,
    }) : Promise<{ 
        buyer_email: string, 
        totalprice: number | string
        history: OrderHistory
    }> => { 
        const order = await removeOrderById({order_uid, redis, repository})
        const store : StoreCache | null | undefined = await redis.get<StoreCache[]>(
            "stores",
            logPath,
        ).then(res => {
            if(res === null) return null
            return res.find(s => s.storeId === order.store_uid)
        })
        if(store === null || store === undefined) throw ERROR.Accepted

        const history = {...} satisfies OrderHistory
        await service.addOrderHistory(order.buyer_email, history)
        
        return { buyer_email: order.buyer_email, totalprice: order.totalprice, history }
    }

주문이 끝난 정보를 삭제하고 삭제된 정보로 주문내역을 생성해 유저의 주문내역 정보를 업데이트 해주고 주문자의 정보와 동기화에 필요한 데이터를 함께 반환 해준다.
(addOrderHistory 함수에서 캐시 정보또한 함께 업데이트 된다.)

마치며

이렇게 소켓까지 정리가 끝나게 되었다.

많이 길어질거 같아 보였지만 막상하고나니 별로 시간이 걸리지는 않았고 다만 다음부터 더 생각하고 작성해야할 부분이 계속해서 보이니 이래서 정리를 하나보구나 싶다.

사이렌 오더 프로젝트는 여기서 끝났고 이제는 개선점을 찾아 보완하고 다음 프로젝트 준비를 하게 될거 같다.

이번 프로젝트는 정말 아쉬움이 크게 남는 프로젝트인 만큼, 남는 것도 많은 프로젝트로 많이 배우고 많이 깨닫는 프로젝트 였다.

profile
개발 블로그보단 개발 일기 입...껄요?

0개의 댓글