서버 정리2

뜨루루루·2024년 1월 12일

sirenorder

목록 보기
7/9

상점

오늘 정리할 부분은 상점! 생각 보다 얽힌게 많아 상인 부분을 같이 엮어서 하고 소켓은 따로 정리 하도록 하겠다.

우선 상인!

모바일 앱에서 유저가 있듯이 상점에는 관리하고 운영하는 상인이 있는 법 사실 정리라고 할 것도 없고 회원가입만 있는데 유저랑 아주 조금 달라서 그래도 적어는 놔야...

상인 등록

상인에게는 총 3가지 고유 아이디가 지급 되는데 순서대로
로그인용, 상점 지갑용, 가게 접근용으로 나뉜다.

uuid를 사용할 때마다 경우의 수가 많더라도 겹칠수는 있지 않나? 생각이 들어서 유저든 상인이든 회원가입시 중복을 재귀로 처리 하는것도 나쁘지 않다 생각 했는데 배포를 할건 아니라서 뺏다.

그 외에는 유저와 프로세스가 그리 다르지 않고 오히려 간략한데 딱히 뭐 해줄게 많이 없어보여서 바로 DB와 캐시에 업데이트 해줬다.

async registMerchant(createData: MerchantDto)
    : Promise<{ merchant: string, store: string, wallet: string }> {
        const { hash, salt } = this.auth.encryption({ pass: createData.pass })
        const uuids = {
            merchant: this.auth.getRandUUID(), 
            store: this.auth.getRandUUID(), 
            wallet: this.auth.getRandUUID(),
        }
        const merchant = await this.merchantRepository.create({
            createData,
            uuids,
            pass: hash,
            salt,
        })
        this._upsertCache(merchant.store)
        return uuids
    }

물론 비밀번호는 단방향 방식으로 유저와 똑같이 진행되며 그 이후는 앞서 말한 3가지 고유 번호를 생성하고 업데이트 해준다.

네 끝났어요 다음 입니다

상점은 미들웨어도 많고 서비스 코드도 상당량 있어서 아마좀 길어질텐데 하나씩 풀어보도록 하자!

단순 조회는 간단하게

조회루틴은 뭐 페이징으로 처리하는게 일반적이지만 그 또한 최초 설계시 고려하지 않았기 때문에 모든 상점 정보를 받아와 앱에서 처리한다.

근데 여기서 상점 정보를 가져올때 상점의 상세정보까지 같이 던져줄까 아니면 분할을 할까 고민을 좀 했는데 아무리 생각해도 한꺼번에 던져주면 받는 입장에서도 부담되고 서버도 부담이 크고 무엇보다 트래픽이 장난아닐거 같아서 차라리 요청을 더 받자 라는 생각으로 분할했다.

(보통 이런 생각으로 조회할때 페이징으로 처리하지 않나? 모르겠다.)

결재

결제는 크게 두 가지 방법으로 나뉘는데 쿠폰결제와 일반결제다.

일반결제는 pg사를 거쳐서 걸제하는 방법인데 여기서도 결제 방법이 카드, 무통장, 가상계좌 등등등 여러가지 있지만 테스트 환경결제만 사용하였기에 카카오 페이만 된다.

일반결제

결제API중, 포트원을 사용했는데 원래는 토스 페이먼츠로 하려다가 사용할때 간편한게 포트원이 압도해서 포트원을 사용했다.

포트원은 최초 글에도 작성했듯이 웹훅을 통해 결제결과를 받아와 처리 할 수 있는데 이 결과에 custom_data 형식으로 임의값을 넣어 전달할 수 있어서 매우 좋았다.

최초 웹훅으로 결과가 날라오면 서비스단에 있는 paymentFactory 함수로 넘어가 유효성 검사를 마친 이후에 어떤 방식의 결제인지 판별하고 그에 맞는 함수를 호출한다.

(유효성 검사라고 해서 빡빡하게 하진 않았고 고유번호 비교만 했습니다.)

보통 포트원 공식문서에서도 자체적인 유효성 검사를 따로 하는걸 추천하고
돈이 걸린 사안이다보니 철저한 검사를 하기를 적극 지향한다.

여기서 사용되는 imp_uid는 이름은 같은데 정보와 사용용도가 다른 두 가지가 있다.

하나는 결제 결과로 날라오는 데이터와 상점이 가지게 되는 고유한 데이터 이다.

이름을 왜 똑같이 지었냐면...
죄송합니다 생각이 짧았습니다......

이를 바탕으로 구현한 코드이다.

async paymentFactory({
        order_uid,
        imp_uid,
        status,
    } : {
        order_uid: string,
        imp_uid: string,
        status: string,
    })
    : Promise<void> {
        const order: OrderDto = await PortOneMethod.findOrder(imp_uid)
        const userUUIDs = { order_uid, imp_uid }
        const portOneUUIDs = { order_uid: order.merchant_uid, imp_uid: order.imp_uid }
        if(!this._equalUUIds(portOneUUIDs, userUUIDs)) {
            this._refuseOrder(order_uid)
            var err = ERROR.BadRequest
            err.substatus = "ForgeryData"
            throw err
        }
        
        let { type } = JSON.parse(order.custom_data)
        switch(type) {
            case "order":
                await this.sendOrder({
                    order_uid,
                    order,
                })
                break
            case "gift":
                await this.sendGift({
                    order_uid,
                    imp_uid,
                    order,
                })
                break
        }
    }

주문 알리기

결제 방식이 왜 주문과 선물이냐 의문이 들겠지만 쿠폰결제는 웹훅을 거치지 않아 여기서 나오는 타입은

주문을 바로 할거냐 아니면 이거 선물이냐로 생각해주시면 되겠다.

무튼 주문을 바로 보내는 부분부터 보자면 이렇다

async sendOrder({
        order_uid,
        order,
    } : {
        order_uid: string,
        order: OrderDto,
    })
    : Promise<boolean> {
        let result = false

        let { data } = JSON.parse(order.custom_data)
        let { storeId, orderInfo } : { storeId: string, orderInfo: OrderInfo } = JSON.parse(data)
        const store = (await this.redis.get<StoreCache[]>("stores", StoreService.name))
        ?.find(s => s.storeId === storeId)

        if(store && store.isOpen && store.socketId) {
            if(!orderInfo) {
                this._refuseOrder(order_uid)
                var err = ERROR.BadRequest
                err.substatus = "ForgeryData"
                throw err
            } else if(!store.socketId) {
                this._refuseOrder(order_uid)
                throw ERROR.Forbidden
            }

            const { menus, deliveryinfo, point } = orderInfo
            const sales_uid = this.authService.getRandUUID()
            const orderEntity = {...} satisfies RegisteredOrder

            await this.redis.set(orderEntity.uuid, orderEntity, StoreService.name)
            .then(async () => {
                if(point !== undefined) {
                    await this.userService
                    .decreaseUserPoint(order.buyer_email!, point)
                }
            })
            .catch(err => {
                this._refuseOrder(order_uid)
                Logger.error("주문정보 캐싱 실패", StoreService.name)
                this.socket.pushStateMessage(order.buyer_email!, "refuse")
                throw err
            })
            result = this.socket.sendOrder(store.socketId, orderEntity)
        }
        if(result) this.socket.pushStateMessage(order.buyer_email!, "wait")
        else {
            this._refuseOrder(order_uid)
            this.socket.pushStateMessage(order.buyer_email!, "refuse")
            Logger.error("주문정보 전달 실패", StoreService.name)
        }
        return result
    }

위에 보면 커스텀 데이터를 파싱하는데 왜 팩토리 부분에서 파싱한걸 넘겨주지 않고 또 다시 파싱해서 사용하냐 생각할텐데 이는 뒤에 나올 선물하기의 커스텀 데이터와 이 주문에 커스텀 데이터가 서로다른 타입이기 때문이다.
(타입은 공통적으로 들어간 속성이기에 상관없다.)

이 데이터를 파싱해서 상점 아이디와 주문정보를 가져오는데 이 상점아이디로 상점을 조회해서 등록되어 있는 상점이고 상점이 현재 영업중이라면 주문을 진행하고 아니라면 오류를 던진다.

그 후에 등록할 주문정보를 가공하여 캐시에 저장하고 상점과 유저에게 처리된 결과를 날려준다.

이 이후에 과정은 상점 서비스단에서 처리되는게 아닌 소켓에서 처리되는 부분이라 이 부분은 소켓정리 글이 올라가면 여기에 이어 볼 수 있도록 해당 부분을 링크로 남겨두겠다.

선물하기

이어서 선물하기 루틴을 보자면 이렇다.

async sendGift({
        order_uid,
        imp_uid,
        order,
    } : {
        order_uid: string,
        imp_uid: string,
        order: OrderDto,
    }) 
    : Promise<void> {
        let { data } = JSON.parse(order.custom_data)
        let { giftInfo } : { giftInfo : GiftInfo } = JSON.parse(data)

        if(!giftInfo) {
            this._refuseOrder(order_uid)
            var err = ERROR.BadRequest
            err.substatus = "ForgeryData"
            throw err
        }
        const message = giftInfo.message ?? ""
        const gift = await this.couponService
        .sendGift(
        {...} as GiftInfo)
        this.socket.pushGiftMessage(gift)
    }

최초 과정은 일반 주문과 다를바가 없고, 혹여나 데이터가 비어있을수 있으니 간단한 처리를 해주고 쿠폰 서비스에 선물정보를 인자로 보내준다.

요거는 다른 서비스 단이긴 하지만 서로 의존하고 있기때문에 요 부분만 여기다가 추가 하겠다.

async sendGift(giftInfo: GiftInfo) : Promise<GiftEntity> {
        const code = await this.publishCoupon({
            menuinfo: giftInfo.menu,
            expiration_day: 1,
        })
        const data = await this._validate(code)
        const coupon = {...} as SimpleCouponEntity
        const gift_uid = this.auth.getRandUUID()
        await this.couponRepoistory.updateGift({
            uuid: gift_uid,
            gift: giftInfo,
            coupon,
        })
        this._updateCouponFromUser(
            giftInfo.to,
            code,
            data.coupon,
        )
        return {...} satisfies GiftEntity
    }

쿠폰 서비스단에서 메뉴정보를 받아와 쿠폰을 발행하는데 여기서 유효기간은 1일로 정해두고 발행했다.

그 밑에 발행된 코드가 유효하게 잘 나왔는지 체크하는 부분이 있는데 이거는 솔직히 지금보면 왜 넣었나 싶다.

큰일이다 벌써부터 고쳐야할 부분이 계속해서 보이기 시작한다.

쿠폰발행을 하면서 발행된 쿠폰을 던져주고 그 쿠폰데이터로 처리를 하면 되는데 왜 저렇게 한걸까?

무튼 그때문에 유효성 검사에서도 쿼리문을 날리는데 그로인해 의미없는 DB서버에 요청이 가고 있던것이다... 지금 보면서 실시간으로 수정하고 있는데 암담하다.

무튼 선물을 보낼 유저에게 선물정보를 업데이트 해주고, 선물정보를 반환 받아 선물받은 유저에게 메세지를 날려 선물이 왔음을 알려준다.

쿠폰결제

쿠폰결제 루틴보다가 또또또 의미없는 루틴이 돌고 있어서 수정했다.

코드 작성할때 정신이 그렇게 없었나?... 싶을 정도로 호출 안해도 될걸 호출하고 이미 가지고 있는 데이터로 처리가능할걸 또 찾고 참... 이래서 혼자서 하는게 힘들다는게 뼈저리게 느껴지는게 하나에 집중할 수 없으니 빵꾸가 계속난다.

어찌되었건 일단은 쿠폰 루틴을 보자...

코드를 보기전에 알아야 할 전제가 쿠폰은 발행과,
등록으로 나뉘어져 있고 등록을 해야 사용이 가능하다.
async useCoupon(
        storeId: string,
        user_email: string, 
        code: string,
        deliveryinfo: DeliveryInfo,
    ) : Promise<{ message?: string, result: boolean }> {
        const store = await this._getStore(storeId)
        if(store === undefined) {
            return {
                message: "영업중인 매장이 아닙니다.",
                result: false,
            }
        }
        const useResult = await this.couponService.checkValidateAndUpdateCoupon(
            user_email,
            code,
        )
        if(typeof useResult === "boolean") {
            return {
                message: "유효하지 않은 쿠폰사용으로 주문이 취소되었습니다",
                result: useResult,
            }
        } else {
            const uuid = this.authService.getRandUUID()
            const sales_uid = this.authService.getRandUUID()
            const order = {...} satisfies RegisteredOrder
            await this.redis.set(order.uuid, order, StoreService.name)
            .catch(err => {
                this._refuseOrder(order.uuid)
                Logger.error("주문정보 캐싱 실패", StoreService.name)
                this.socket.pushStateMessage(user_email, "refuse")
                throw err
            })
            const result = this.socket.sendOrder(store.socketId!, order)
            if(result) this.socket.pushStateMessage(user_email, "wait")
            else {
                this._refuseOrder(order.uuid)
                this.socket.pushStateMessage(order.buyer_email!, "refuse")
                Logger.error("주문정보 전달 실패", StoreService.name)
            }
            return { result }
        }
    }

맨 위에 상점을 불러오는 함수가 있는데 저 부분이 바뀐부분이고 주문날리기 부분에도 현재는 저렇게 수정되어있다.

상점을 불러오는 함수에서 상점 존재여부, 영업여부 등을 판별해 넘겨주기 때문에 온전히 상점정보가 넘어온다면 상점은 유효한 상태인 것으로 판단하고 쿠폰의 유효성을 판단하고 유저 데이터를 업데이트 한다.

그 후는 주문날리기와 루틴이 동일하다.

마치며

생각만큼 엄청 길었고 이거 정리하고 간간히 수정하느라
1시간 반 정도 걸린거 같은데 아직 너무 미숙한게 느껴질만큼 코드가 불량하다...

아 그리고 이번 정리가 좀 늦었는데 중간에 이력서를 넣었던 기업에서 코테를 보라고 날라와 준비를 조금 했다.

본래는 더 늦어질 예정이였지만 시험장 입실 이후에 퇴장이 불가하단 문구를 입장하고서 봐버린 탓에 새벽까지 해버리느라 시일이 앞 당겨졌다.

(온라인 코테라 기간내에 풀어서 제출하기만 하면 됩니다.)

다음 정리를 기약하며 총총...

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

0개의 댓글