Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/features/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ export class AuthService {
*/
async logout(req: Request, res: Response): Promise<void> {
const refreshToken = req.cookies?.[AUTH_COOKIE.REFRESH] as
| string
| undefined;
string | undefined;

if (refreshToken) {
const tokenHash = this.tokens.sha256Hex(refreshToken);
Expand Down
9 changes: 3 additions & 6 deletions src/features/auth/services/oidc-login.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,11 @@ export class OidcLoginService implements IOidcLoginService {
returnTo: string;
} {
const expectedState = req.cookies?.[AUTH_COOKIE.OIDC_STATE] as
| string
| undefined;
string | undefined;
const expectedNonce = req.cookies?.[AUTH_COOKIE.OIDC_NONCE] as
| string
| undefined;
string | undefined;
const codeVerifier = req.cookies?.[AUTH_COOKIE.OIDC_CODE_VERIFIER] as
| string
| undefined;
string | undefined;
const returnTo =
(req.cookies?.[AUTH_COOKIE.OIDC_RETURN_TO] as string | undefined) ??
this.normalizeReturnTo(undefined);
Expand Down
3 changes: 1 addition & 2 deletions src/features/auth/services/seller-credential.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ export class SellerCredentialService implements ISellerCredentialService {

async logoutSeller(req: Request, res: Response): Promise<void> {
const refreshToken = req.cookies?.[AUTH_COOKIE.REFRESH] as
| string
| undefined;
string | undefined;

if (!refreshToken)
throw new UnauthorizedException('Missing refresh token.');
Expand Down
3 changes: 1 addition & 2 deletions src/features/auth/services/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ export class TokenService implements ITokenService {
res: Response,
): Promise<{ accessToken: string; accountId: bigint }> {
const refreshToken = req.cookies?.[AUTH_COOKIE.REFRESH] as
| string
| undefined;
string | undefined;

if (!refreshToken) {
throw new UnauthorizedException('Missing refresh token.');
Expand Down
6 changes: 1 addition & 5 deletions src/features/seller/services/seller-banner.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,7 @@ export class SellerBannerService
) {
return {
linkType: (input.linkType ?? current.link_type) as
| 'NONE'
| 'URL'
| 'PRODUCT'
| 'STORE'
| 'CATEGORY',
'NONE' | 'URL' | 'PRODUCT' | 'STORE' | 'CATEGORY',
linkProductId:
input.linkProductId !== undefined
? input.linkProductId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ export interface AuditLogRow {
actor_account_id: bigint;
store_id: bigint | null;
target_type:
| 'STORE'
| 'PRODUCT'
| 'ORDER'
| 'CONVERSATION'
| 'CHANGE_PASSWORD';
'STORE' | 'PRODUCT' | 'ORDER' | 'CONVERSATION' | 'CHANGE_PASSWORD';
target_id: bigint;
action: 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE';
before_json: Prisma.JsonValue | null;
Expand Down
13 changes: 2 additions & 11 deletions src/features/seller/types/seller-output.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,7 @@ export interface SellerOrderSummaryOutput {
export interface SellerOrderStatusHistoryOutput {
id: string;
fromStatus:
| 'SUBMITTED'
| 'CONFIRMED'
| 'MADE'
| 'PICKED_UP'
| 'CANCELED'
| null;
'SUBMITTED' | 'CONFIRMED' | 'MADE' | 'PICKED_UP' | 'CANCELED' | null;
toStatus: 'SUBMITTED' | 'CONFIRMED' | 'MADE' | 'PICKED_UP' | 'CANCELED';
changedAt: Date;
note: string | null;
Expand Down Expand Up @@ -276,11 +271,7 @@ export interface SellerAuditLogOutput {
actorAccountId: string;
storeId: string | null;
targetType:
| 'STORE'
| 'PRODUCT'
| 'ORDER'
| 'CONVERSATION'
| 'CHANGE_PASSWORD';
'STORE' | 'PRODUCT' | 'ORDER' | 'CONVERSATION' | 'CHANGE_PASSWORD';
targetId: string;
action: 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE';
beforeJson: string | null;
Expand Down
2 changes: 2 additions & 0 deletions src/features/store/repositories/store.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface StoreCandidateRow {
region: { name: string } | null;
pickup_slot_interval_minutes: number;
min_lead_time_minutes: number;
max_days_ahead: number;
}

/** 특정 요일의 매장 영업시간 row(오늘 픽업 슬롯 산출용). */
Expand Down Expand Up @@ -89,6 +90,7 @@ export class StoreRepository {
region: { select: { name: true } },
pickup_slot_interval_minutes: true,
min_lead_time_minutes: true,
max_days_ahead: true,
},
});
}
Expand Down
1 change: 1 addition & 0 deletions src/features/store/services/store-mappers.helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function row(overrides: Partial<StoreCandidateRow>): StoreCandidateRow {
region: null,
pickup_slot_interval_minutes: 30,
min_lead_time_minutes: 30,
max_days_ahead: 7,
...overrides,
};
}
Expand Down
164 changes: 164 additions & 0 deletions src/features/store/services/store-pickup-policy.helper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { kstMidnightUtc } from '@/common/utils/kst-time';
import {
buildPickupDaySlots,
evaluatePickupDay,
type PickupDayInput,
} from '@/features/store/services/store-pickup-policy.helper';

const HOUR_MS = 60 * 60 * 1000;

/** @db.Time(0) 컬럼 표현(1970-01-01 UTC 기반)으로 시각 생성. */
function timeCol(hours: number, minutes = 0): Date {
return new Date(Date.UTC(1970, 0, 1, hours, minutes));
}

/** 해당 KST 날짜의 hh시(KST) 시각. */
function kstAt(year: number, month: number, day: number, hours: number): Date {
return new Date(kstMidnightUtc(year, month, day).getTime() + hours * HOUR_MS);
}

describe('store-pickup-policy.helper', () => {
// 기준일: 2026-09-10(KST), 영업 10:00~18:00, 간격 30분, 리드타임 60분, 7일 전 예약
const DAY = { year: 2026, month: 9, day: 10 };
const dayStartUtc = kstMidnightUtc(DAY.year, DAY.month, DAY.day);

function input(overrides: Partial<PickupDayInput> = {}): PickupDayInput {
return {
store: {
pickup_slot_interval_minutes: 30,
min_lead_time_minutes: 60,
max_days_ahead: 7,
},
hour: {
is_closed: false,
open_time: timeCol(10),
close_time: timeCol(18),
},
isSpecialClosure: false,
capacity: undefined,
booked: 0,
now: kstAt(DAY.year, DAY.month, DAY.day, 9),
dayStartUtc,
...overrides,
};
}

describe('evaluatePickupDay', () => {
it('정상 영업일은 reason null과 가용 슬롯을 반환한다', () => {
const result = evaluatePickupDay(input());
expect(result.reason).toBeNull();
// 10:00~17:30, 30분 간격 16개. 09:00+리드 60분 → 10:00부터 전부 가용
expect(result.slots).toHaveLength(16);
expect(result.slots.every((slot) => slot.available)).toBe(true);
});

it('과거 날짜는 PAST를 반환한다', () => {
const result = evaluatePickupDay(
input({ now: kstAt(DAY.year, DAY.month, DAY.day + 1, 9) }),
);
expect(result.reason).toBe('PAST');
});

it('max_days_ahead를 넘는 날짜는 OUT_OF_RANGE, 경계일은 허용한다', () => {
const boundary = evaluatePickupDay(
input({ now: kstAt(DAY.year, DAY.month, DAY.day - 7, 9) }),
);
expect(boundary.reason).toBeNull();

const beyond = evaluatePickupDay(
input({ now: kstAt(DAY.year, DAY.month, DAY.day - 8, 9) }),
);
expect(beyond.reason).toBe('OUT_OF_RANGE');
});

it('특별휴무일은 CLOSED이되 마감 표기용 슬롯은 반환한다', () => {
const result = evaluatePickupDay(input({ isSpecialClosure: true }));
expect(result.reason).toBe('CLOSED');
expect(result.slots).toHaveLength(16);
});

it('영업시간 미설정·휴무 요일·시간 누락은 CLOSED와 빈 슬롯을 반환한다', () => {
const cases: (PickupDayInput['hour'] | undefined)[] = [
undefined,
{ is_closed: true, open_time: timeCol(10), close_time: timeCol(18) },
{ is_closed: false, open_time: null, close_time: timeCol(18) },
{ is_closed: false, open_time: timeCol(10), close_time: null },
];
for (const hour of cases) {
const result = evaluatePickupDay(input({ hour }));
expect(result.reason).toBe('CLOSED');
expect(result.slots).toEqual([]);
}
});

it('점유 수량이 capacity에 도달하면 CAPACITY_FULL, 미달이면 통과한다', () => {
expect(evaluatePickupDay(input({ capacity: 5, booked: 5 })).reason).toBe(
'CAPACITY_FULL',
);
expect(
evaluatePickupDay(input({ capacity: 5, booked: 4 })).reason,
).toBeNull();
});

it('capacity 레코드가 없으면 무제한으로 간주한다', () => {
expect(
evaluatePickupDay(input({ capacity: undefined, booked: 999 })).reason,
).toBeNull();
});

it('리드타임 반영 잔여 가용 슬롯이 없으면 CLOSED로 마감한다', () => {
// 17:00 + 리드 60분 = 18:00 컷오프 → 17:30 슬롯까지 전부 마감
const result = evaluatePickupDay(
input({ now: kstAt(DAY.year, DAY.month, DAY.day, 17) }),
);
expect(result.reason).toBe('CLOSED');
expect(result.slots).toHaveLength(16);
expect(result.slots.every((slot) => !slot.available)).toBe(true);
});

it('과거이면서 특별휴무여도 PAST가 우선한다(판정 순서 고정)', () => {
const result = evaluatePickupDay(
input({
now: kstAt(DAY.year, DAY.month, DAY.day + 1, 9),
isSpecialClosure: true,
}),
);
expect(result.reason).toBe('PAST');
});
});

describe('buildPickupDaySlots', () => {
const store = {
pickup_slot_interval_minutes: 30,
min_lead_time_minutes: 60,
max_days_ahead: 7,
};

it('미래 날짜는 경과 분이 음수가 되어 컷오프가 앞당겨진다(전 슬롯 가용)', () => {
const slots = buildPickupDaySlots(
store,
timeCol(10),
timeCol(18),
dayStartUtc,
kstAt(DAY.year, DAY.month, DAY.day - 3, 23),
);
expect(slots.every((slot) => slot.available)).toBe(true);
});

it('초 단위가 남으면 다음 분으로 올려 리드타임을 보수적으로 적용한다', () => {
// 09:00:30 + 리드 60분: 분 절삭이면 10:00 가용이지만, 올림으로 10:00은 마감
const now = new Date(
kstAt(DAY.year, DAY.month, DAY.day, 9).getTime() + 30 * 1000,
);
const slots = buildPickupDaySlots(
store,
timeCol(10),
timeCol(18),
dayStartUtc,
now,
);
expect(slots[0]).toEqual({ time: '10:00', available: false });
expect(slots[1]).toEqual({ time: '10:30', available: true });
});
});
});
Loading
Loading