From 107f1ffe8483e2bd6cab2a23d1d3128ce1602d9c Mon Sep 17 00:00:00 2001 From: hyoseung930 <35983843+hyoseung930@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:29:13 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 수정 내용 - **Sign Error 근본 원인 2가지 수정**: 1. `now()` 함수가 **초** 단위(`Date.now()/1000`)를 반환하고 있었음 → **밀리초**(`Date.now()`)로 수정 (설계서 명시 사항) 2. 세션 쿠키 추출 시 쿠키 이름에 'session'이 없으면 빈 값으로 보내고 있었음 → **모든 쿠키를 전체 전송**으로 수정 - **쿠폰 코드 대소문자 유지**: `toUpperCase()` 제거 - **FID localStorage 저장**: 한번 입력하면 다음 방문 시 자동으로 로드되어 바로 쿠폰 지급 시작 - **쿠폰 추가 시 자동 리딤**: 새 쿠폰 등록하면 DB에 저장된 **모든 유저**에게 백그라운드로 자동 지급 --- back/src/wos/wos.service.ts | 63 ++++++++++++++++++++++---- front/src/components/CouponManager.vue | 2 +- front/src/stores/wos.store.ts | 1 + front/src/views/HomeView.vue | 13 +++++- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/back/src/wos/wos.service.ts b/back/src/wos/wos.service.ts index e9b1faf..069d7b3 100644 --- a/back/src/wos/wos.service.ts +++ b/back/src/wos/wos.service.ts @@ -41,8 +41,12 @@ export class WosService { return crypto.createHash('md5').update(raw).digest('hex'); } + private now(): number { + return Date.now(); + } + async getPlayer(fid: string) { - const timestamp = Date.now(); + const timestamp = this.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( @@ -74,7 +78,7 @@ export class WosService { } private async acquireSession(fid: string): Promise { - const timestamp = Date.now(); + const timestamp = this.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( @@ -84,12 +88,10 @@ export class WosService { ); const setCookie = response.headers['set-cookie']; - let sessionValue = ''; - if (setCookie) { - const found = setCookie.find((c: string) => c.toLowerCase().includes('session')); - if (found) sessionValue = found.split(';')[0]; + if (setCookie && setCookie.length > 0) { + return setCookie.map((c: string) => c.split(';')[0]).join('; '); } - return sessionValue; + return ''; } private async redeemOne( @@ -97,7 +99,7 @@ export class WosService { code: string, session: string, ): Promise<{ success: boolean; message: string; err_code?: number }> { - const timestamp = Date.now(); + const timestamp = this.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( @@ -201,9 +203,50 @@ export class WosService { exists.name = name; exists.is_active = true; exists.is_expired = false; - return this.wosCouponRepo.save(exists); + await this.wosCouponRepo.save(exists); + } else { + await this.wosCouponRepo.save({ name, code, is_active: true, is_expired: false }); + } + + this.redeemCouponForAllUsers(code).catch((err) => + this.logger.error(`신규 쿠폰 전체 지급 실패: ${err.message}`), + ); + + return this.wosCouponRepo.findOne({ where: { code } }); + } + + private async redeemCouponForAllUsers(code: string) { + const users = await this.wosUserRepo.find(); + for (const user of users) { + const alreadyDone = await this.couponLogRepo.findOne({ + where: { fid: user.fid, coupon_code: code, result_msg: In([SUCCESS_MSG, ALREADY_MSG]) }, + }); + if (alreadyDone) continue; + + try { + const session = await this.acquireSession(user.fid); + const result = await this.redeemOne(user.fid, code, session); + + const finalResult = + result.err_code === 40008 + ? { success: true, message: ALREADY_MSG } + : result; + + if (result.err_code === 40007) { + const coupon = await this.wosCouponRepo.findOne({ where: { code } }); + if (coupon) await this.wosCouponRepo.update(coupon.id, { is_expired: true, is_active: false }); + await this.couponLogRepo.save({ fid: user.fid, coupon_code: code, result_msg: '만료된 쿠폰' }); + break; + } + + await this.couponLogRepo.save({ fid: user.fid, coupon_code: code, result_msg: finalResult.message }); + this.logger.log(`[${user.fid}] ${code} → ${finalResult.message}`); + } catch (err: any) { + this.logger.error(`[${user.fid}] ${code} 지급 실패: ${err.message}`); + } + + await new Promise((r) => setTimeout(r, 1500)); } - return this.wosCouponRepo.save({ name, code, is_active: true, is_expired: false }); } async toggleCoupon(id: number, is_active: boolean) { diff --git a/front/src/components/CouponManager.vue b/front/src/components/CouponManager.vue index fe6cf69..ba12a48 100644 --- a/front/src/components/CouponManager.vue +++ b/front/src/components/CouponManager.vue @@ -48,7 +48,7 @@ const activeCoupons = computed(() => store.coupons.filter((c) => c.is_active && async function add() { if (!newCode.value.trim()) return; - const code = newCode.value.trim().toUpperCase(); + const code = newCode.value.trim(); await store.addCoupon(code, code); newCode.value = ''; } diff --git a/front/src/stores/wos.store.ts b/front/src/stores/wos.store.ts index c571e55..f7ae34b 100644 --- a/front/src/stores/wos.store.ts +++ b/front/src/stores/wos.store.ts @@ -60,6 +60,7 @@ export const useWosStore = defineStore('wos', { const { data } = await axios.post('/api/player', { fid }); this.player = data; this.fid = fid; + localStorage.setItem('wos_fid', fid); await Promise.all([this.loadHistory(), this.redeemAll()]); this.status = 'idle'; } catch (err: any) { diff --git a/front/src/views/HomeView.vue b/front/src/views/HomeView.vue index c3a12d4..74a80da 100644 --- a/front/src/views/HomeView.vue +++ b/front/src/views/HomeView.vue @@ -68,19 +68,28 @@ import CouponManager from '../components/CouponManager.vue'; const store = useWosStore(); const fidInput = ref(''); -onMounted(() => { +onMounted(async () => { store.loadCoupons(); store.loadSavedUsers(); + const savedFid = localStorage.getItem('wos_fid'); + if (savedFid) { + fidInput.value = savedFid; + await store.searchPlayer(savedFid); + store.loadSavedUsers(); + } }); async function searchPlayer() { if (!fidInput.value.trim()) return; - await store.searchPlayer(fidInput.value.trim()); + const fid = fidInput.value.trim(); + localStorage.setItem('wos_fid', fid); + await store.searchPlayer(fid); store.loadSavedUsers(); } async function selectUser(fid: string) { fidInput.value = fid; + localStorage.setItem('wos_fid', fid); await store.searchPlayer(fid); store.loadSavedUsers(); }