Skip to content

Commit fd8f0d7

Browse files
SOIVclaude
andcommitted
refactor(core): 브라우저 인증 유틸리티 코어 집중화
@fieldstack/core/browser (browser-fetch.ts)에 추가: - apiCall<T>: apiFetch + Fieldstack JSON 파싱 ({ success, data, error }) 표준 래퍼 - Content-Type: application/json 자동 첨부 - 빈 응답(204) 안전 처리 - 모듈 api.ts의 공통 패턴을 코어 한 곳으로 집중 - FS_TOKEN, FS_REFRESH: sessionStorage 키 상수 export 적용: - ChangePasswordView: fetch + 수동 토큰 → apiCall로 교체 - AdminPinModal: fetch + 수동 토큰 → apiFetch로 교체 - LedgerView: 로컬 typed wrapper 제거, apiCall 직접 사용 - lib/apiFetch.ts: apiCall, FS_TOKEN, FS_REFRESH re-export 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7a8e3af commit fd8f0d7

5 files changed

Lines changed: 52 additions & 37 deletions

File tree

apps/web/src/components/AdminPinModal.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useEffect, useState, type FormEvent } from "react";
22

33
import { Button, Modal, PinInput } from "@fieldstack/controls";
44

5+
import { apiFetch } from "../lib/apiFetch";
6+
57
const MAX_ATTEMPTS = 5;
68
const LOCKOUT_SECONDS = 300; // 5분
79

@@ -45,13 +47,9 @@ export function AdminPinModal({ onVerified, onClose }: AdminPinModalProps) {
4547

4648
setIsVerifying(true);
4749
try {
48-
const token = sessionStorage.getItem("fs_token") ?? "";
49-
const res = await fetch("/admin/verify-pin", {
50+
const res = await apiFetch("/admin/verify-pin", {
5051
method: "POST",
51-
headers: {
52-
"Content-Type": "application/json",
53-
Authorization: `Bearer ${token}`,
54-
},
52+
headers: { "Content-Type": "application/json" },
5553
body: JSON.stringify({ pin }),
5654
});
5755

apps/web/src/lib/apiFetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
* @fieldstack/core/browser의 apiFetch를 re-export합니다.
33
* 실제 구현은 packages/core/src/browser-fetch.ts에 있습니다.
44
*/
5-
export { apiFetch, setSessionExpiredHandler } from '@fieldstack/core/browser';
5+
export { apiFetch, apiCall, setSessionExpiredHandler, FS_TOKEN, FS_REFRESH } from '@fieldstack/core/browser';

apps/web/src/views/ChangePasswordView.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useState, type FormEvent } from "react";
22

33
import { PASSWORD_POLICY, validatePassword } from "@fieldstack/core/browser";
44

5+
import { apiCall } from "../lib/apiFetch";
6+
57
import { Alert, Button, FormField, Input } from "@fieldstack/controls";
68

79
import "../styles/change-password.css";
@@ -37,17 +39,10 @@ export function ChangePasswordView({ isFirstLogin, onChanged }: ChangePasswordVi
3739
if (!canSubmit) return;
3840
setApiError("");
3941
try {
40-
const token = sessionStorage.getItem("fs_token") ?? "";
41-
const res = await fetch("/auth/password/change", {
42+
await apiCall("/auth/password/change", {
4243
method: "POST",
43-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
4444
body: JSON.stringify({ newPassword: next.value }),
4545
});
46-
const json = await res.json() as { success: boolean; error?: string };
47-
if (!res.ok || !json.success) {
48-
setApiError(json.error ?? "비밀번호 변경에 실패했습니다.");
49-
return;
50-
}
5146
onChanged();
5247
} catch {
5348
setApiError("서버 연결 오류. 다시 시도해주세요.");

modules/ledger/frontend/LedgerView.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
Spinner,
1313
} from "@fieldstack/controls";
1414
import type { TableColumn } from "@fieldstack/controls";
15-
// apiFetch: @fieldstack/core/browser의 re-export (토큰 갱신·세션 만료 처리 포함)
16-
import { apiFetch as coreFetch } from "../../../apps/web/src/lib/apiFetch";
15+
// apiCall: @fieldstack/core/browser의 re-export (토큰 갱신·세션 만료·JSON 파싱 포함)
16+
import { apiCall } from "../../../apps/web/src/lib/apiFetch";
1717

1818
// ── 공유 타입 (modules/ledger/types/index.ts와 동일하게 유지) ─
1919

@@ -71,19 +71,10 @@ interface LedgerSummary {
7171
type EntryRow = LedgerEntry & Record<string, unknown>;
7272

7373
// ── API 헬퍼 ─────────────────────────────────────────────────
74-
// 토큰 갱신·세션 만료 처리는 @fieldstack/core/browser의 apiFetch가 담당한다.
75-
76-
async function apiFetch<T>(path: string, opts?: RequestInit): Promise<T> {
77-
const res = await coreFetch(path, {
78-
...opts,
79-
headers: { "Content-Type": "application/json", ...(opts?.headers ?? {}) },
80-
});
81-
const text = await res.text();
82-
if (!text) return undefined as T;
83-
const json = JSON.parse(text) as { success: boolean; data?: T; error?: unknown };
84-
if (!json.success) throw new Error(String(json.error ?? "API 오류"));
85-
return json.data as T;
86-
}
74+
// apiCall: 토큰 갱신·세션 만료·JSON 파싱을 @fieldstack/core/browser에서 처리
75+
76+
// LedgerView 전용 별칭 — 내부에서 apiFetch<T> 로 호출하던 패턴 유지
77+
const apiFetch = apiCall;
8778

8879
// ── 금액 포맷 ─────────────────────────────────────────────────
8980

packages/core/src/browser-fetch.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
/**
2-
* apiFetch — 인증 토큰 자동 첨부 + 만료 시 자동 갱신
2+
* Fieldstack 브라우저 인증 유틸리티
33
*
4-
* Fieldstack 플랫폼의 모든 모듈이 공유하는 인증 fetch 래퍼.
5-
* Vite 번들 내에서 모듈 싱글턴을 공유하므로 main.tsx에서 등록한
6-
* setSessionExpiredHandler가 모든 모듈의 apiFetch 호출에 적용된다.
4+
* apiFetch — 인증 토큰 자동 첨부 + 만료 시 자동 갱신 (raw Response 반환)
5+
* apiCall — apiFetch + Fieldstack JSON 응답({ success, data, error }) 파싱
6+
* FS_TOKEN, FS_REFRESH — sessionStorage 키 상수
77
*
88
* 사용법:
9-
* import { apiFetch, setSessionExpiredHandler } from '@fieldstack/core/browser';
9+
* import { apiFetch, apiCall, setSessionExpiredHandler } from '@fieldstack/core/browser';
1010
*/
1111

12-
const SS_TOKEN = 'fs_token';
13-
const SS_REFRESH = 'fs_refresh';
12+
/** sessionStorage 인증 토큰 키 */
13+
export const FS_TOKEN = 'fs_token';
14+
/** sessionStorage 리프레시 토큰 키 */
15+
export const FS_REFRESH = 'fs_refresh';
16+
17+
const SS_TOKEN = FS_TOKEN;
18+
const SS_REFRESH = FS_REFRESH;
1419

1520
type SessionExpiredHandler = () => void;
1621
let sessionExpiredHandler: SessionExpiredHandler | null = null;
@@ -87,3 +92,29 @@ export async function apiFetch(input: string, init: RequestInit = {}): Promise<R
8792

8893
return fetch(input, attachAuth(init, newToken));
8994
}
95+
96+
/**
97+
* Fieldstack JSON API 호출 유틸리티.
98+
*
99+
* apiFetch 위에서 동작하며 Fieldstack 표준 응답 형식을 자동으로 파싱한다:
100+
* { success: boolean; data?: T; error?: string }
101+
*
102+
* - Content-Type: application/json 헤더 자동 첨부
103+
* - 빈 응답(204 No Content 등)은 undefined로 반환
104+
* - success: false 시 error 메시지로 Error를 throw
105+
*
106+
* 모듈 api.ts에서 사용하는 표준 패턴:
107+
* import { apiCall } from '@fieldstack/core/browser';
108+
* const data = await apiCall<MyType>('/api/my-module/items');
109+
*/
110+
export async function apiCall<T>(path: string, init?: RequestInit): Promise<T> {
111+
const res = await apiFetch(path, {
112+
...init,
113+
headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
114+
});
115+
const text = await res.text();
116+
if (!text) return undefined as T;
117+
const json = JSON.parse(text) as { success: boolean; data?: T; error?: unknown };
118+
if (!json.success) throw new Error(String(json.error ?? 'API error'));
119+
return json.data as T;
120+
}

0 commit comments

Comments
 (0)