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
85 changes: 84 additions & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"daisoFindStores",
"daisoCheckInventory",
"daisoGetDisplayLocation",
"oliveyoungSearchProducts",
"oliveyoungFindStores",
"oliveyoungCheckInventory",
"cuFindStores",
Expand Down Expand Up @@ -494,7 +495,7 @@
"imageUrl": {
"type": "string",
"description": "제품 이미지 URL",
"example": "https://img.daisomall.co.kr/..."
"example": "https://cdn.daisomall.co.kr/..."
},
"soldOut": {
"type": "boolean",
Expand Down Expand Up @@ -552,6 +553,36 @@
}
}
},
"InventoryProduct": {
"type": "object",
"description": "재고 응답에 포함되는 상품 요약 정보",
"properties": {
"id": {
"type": "string",
"description": "제품 ID"
},
"name": {
"type": "string",
"description": "제품명"
},
"imageUrl": {
"type": "string",
"description": "제품 이미지 URL"
},
"brand": {
"type": "string",
"description": "브랜드명"
},
"soldOut": {
"type": "boolean",
"description": "품절 여부"
},
"isNew": {
"type": "boolean",
"description": "신상품 여부"
}
}
},
"Store": {
"type": "object",
"description": "매장 정보",
Expand Down Expand Up @@ -746,6 +777,9 @@
"type": "string",
"description": "제품 ID"
},
"product": {
"$ref": "#/components/schemas/InventoryProduct"
},
"location": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -849,6 +883,10 @@
"type": "string",
"example": "달바 퍼플 톤업 선크림 듀오 기획"
},
"imageUrl": {
"type": "string",
"example": "https://image.oliveyoung.co.kr/uploads/images/goods/10/0000/0020/A00000020061401ko.jpg"
},
"priceToPay": {
"type": "integer",
"example": 32130
Expand Down Expand Up @@ -1024,6 +1062,51 @@
}
}
},
"OliveyoungProductSearchResponse": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"example": "마스크팩"
},
"count": {
"type": "integer",
"example": 2
},
"products": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OliveyoungProduct"
}
}
}
},
"meta": {
"type": "object",
"properties": {
"total": {
"type": "integer"
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"nextPage": {
"type": "boolean"
}
}
}
}
},
"OliveyoungInventoryResponse": {
"type": "object",
"properties": {
Expand Down
60 changes: 59 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ paths:
- daisoFindStores
- daisoCheckInventory
- daisoGetDisplayLocation
- oliveyoungSearchProducts
- oliveyoungFindStores
- oliveyoungCheckInventory
- cuFindStores
Expand Down Expand Up @@ -350,7 +351,7 @@ components:
imageUrl:
type: string
description: 제품 이미지 URL
example: "https://img.daisomall.co.kr/..."
example: "https://cdn.daisomall.co.kr/..."
soldOut:
type: boolean
description: 품절 여부
Expand Down Expand Up @@ -392,6 +393,28 @@ components:
isNew:
type: boolean
description: 신상품 여부
InventoryProduct:
type: object
description: 재고 응답에 포함되는 상품 요약 정보
properties:
id:
type: string
description: 제품 ID
name:
type: string
description: 제품명
imageUrl:
type: string
description: 제품 이미지 URL
brand:
type: string
description: 브랜드명
soldOut:
type: boolean
description: 품절 여부
isNew:
type: boolean
description: 신상품 여부
Store:
type: object
description: 매장 정보
Expand Down Expand Up @@ -533,6 +556,8 @@ components:
productId:
type: string
description: 제품 ID
product:
$ref: "#/components/schemas/InventoryProduct"
location:
type: object
properties:
Expand Down Expand Up @@ -605,6 +630,9 @@ components:
goodsName:
type: string
example: 달바 퍼플 톤업 선크림 듀오 기획
imageUrl:
type: string
example: "https://image.oliveyoung.co.kr/uploads/images/goods/10/0000/0020/A00000020061401ko.jpg"
priceToPay:
type: integer
example: 32130
Expand Down Expand Up @@ -731,6 +759,36 @@ components:
type: integer
pageSize:
type: integer
OliveyoungProductSearchResponse:
type: object
properties:
success:
type: boolean
example: true
data:
type: object
properties:
keyword:
type: string
example: 마스크팩
count:
type: integer
example: 2
products:
type: array
items:
$ref: "#/components/schemas/OliveyoungProduct"
meta:
type: object
properties:
total:
type: integer
page:
type: integer
pageSize:
type: integer
nextPage:
type: boolean
OliveyoungInventoryResponse:
type: object
properties:
Expand Down
11 changes: 10 additions & 1 deletion src/services/daiso/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@ export const DAISOMALL_API = {
/** 온라인 재고 조회 API */
ONLINE_STOCK: 'https://mapi.daisomall.co.kr/ms/msg/selOnlStck',

/** 매장별 재고 조회 API */
/** 재고 조회 API (현재 403 Unauthorized 발생) */
STORE_INVENTORY: 'https://mapi.daisomall.co.kr/ms/msg/newIntSelStr',

/** 현재 사용 중인 인증 토큰 요청 API */
AUTH_REQUEST: 'https://fapi.daisomall.co.kr/auth/request',

/** 현재 사용 중인 위치 기반 매장 조회 API */
STORE_SEARCH_V2: 'https://fapi.daisomall.co.kr/ms/msg/selStr',

/** 현재 사용 중인 매장별 재고 조회 API */
STORE_INVENTORY_V2: 'https://fapi.daisomall.co.kr/pd/pdh/selStrPkupStck',

/** 매장 내 상품 진열 위치 조회 API */
DISPLAY_LOCATION: 'https://fapi.daisomall.co.kr/pdo/selIntPdStDispInfo',

Expand Down
90 changes: 90 additions & 0 deletions src/services/daiso/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
*/

import { type FetchOptions, fetchJson, fetchText, fetchWithTimeout } from '../../utils/http.js';
import { DAISOMALL_API } from './api.js';

const DAISO_AUTH_KEY = 'PRE_AUTH_ENC_KEY';

const DAISO_DEFAULT_HEADERS = {
'User-Agent':
Expand Down Expand Up @@ -40,3 +43,90 @@ export async function fetchDaisoHtml(url: string, options: FetchOptions = {}): P
headers: withDaisoHeaders(options.headers),
});
}

function base64FromBytes(bytes: Uint8Array): string {
/* c8 ignore next -- Node 20 CI always provides Buffer; the fallback below is for browser-like runtimes. */
if (typeof Buffer !== 'undefined') {
return Buffer.from(bytes).toString('base64');
}

/* c8 ignore start -- Node 20 CI always provides Buffer; this fallback is for browser-like runtimes. */
let binary = '';
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}

if (typeof btoa === 'function') {
return btoa(binary);
}

throw new Error('Base64 인코딩을 지원하지 않는 환경입니다.');
/* c8 ignore stop */
}

async function createDaisoAuthHeader(token: string): Promise<string> {
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(DAISO_AUTH_KEY),
{ name: 'AES-CBC' },
false,
['encrypt'],
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv },
key,
encoder.encode(token),
);

return `${base64FromBytes(iv)}${base64FromBytes(new Uint8Array(encrypted))}`;
}

export interface DaisoAuthContext {
authorization: string;
dmUid: string;
cookie: string;
}

export async function createDaisoAuthContext(): Promise<DaisoAuthContext> {
const response = await daisoFetch(DAISOMALL_API.AUTH_REQUEST, {
method: 'GET',
});

if (!response.ok) {
throw new Error(`다이소 인증 토큰 요청 실패: ${response.status} ${response.statusText}`);
}

const token = (await response.text()).trim();
const dmUid = response.headers.get('x-dm-uid')?.trim() || '';

if (!token) {
throw new Error('다이소 인증 토큰이 비어 있습니다.');
}

if (!dmUid) {
throw new Error('다이소 인증 응답에 X-DM-UID 헤더가 없습니다.');
}

const authorization = await createDaisoAuthHeader(token);
return {
authorization: `Bearer ${authorization}`,
dmUid,
cookie: `DM_UID=${dmUid}`,
};
}

export async function fetchDaisoJsonWithAuth<T>(url: string, options: FetchOptions = {}): Promise<T> {
const auth = await createDaisoAuthContext();

return fetchJson<T>(url, {
...options,
headers: withDaisoHeaders({
...options.headers,
Authorization: auth.authorization,
'X-DM-UID': auth.dmUid,
Cookie: auth.cookie,
}),
});
}
Loading