export type OrgStatus = 'INFO_PENDING' | 'READY' | 'PARTIAL' | 'ACTIVE' export interface Org { id: number orgNo: string orgName: string status: OrgStatus deptName: string | null managerName: string | null managerTitle: string | null managerPhone: string | null managerEmail: string | null channelIdMj: string | null channelIdLaw: string | null } export interface Contact { deptName: string managerName: string managerTitle: string managerPhone: string managerEmail: string } export type ProvisionOutcome = 'CACHED' | 'RECOVERED' | 'CREATED' | 'FAILED' export interface ProvisionResult { mj: ProvisionOutcome law: ProvisionOutcome message: string } export interface SeedReport { created: number updated: number total: number } /** * 서버가 4xx/5xx로 응답했을 때 던지는 예외. status를 함께 들고 있어야 호출자가 * "인증이 끊긴 것(401)"과 "그 밖의 모든 오류"를 구분해서 다르게 처리할 수 있다 - * 그렇지 않으면(예: 평범한 Error) 서버 오류나 네트워크 문제까지 전부 로그아웃으로 * 오인해 사용자를 로그인 화면으로 쫓아내게 된다. */ export class ApiError extends Error { constructor( public status: number, message: string, ) { super(message) this.name = 'ApiError' } } /** Spring Security가 내려주는 CSRF 쿠키를 읽어 헤더로 되돌려 보낸다. */ function csrfToken(): string { const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/) return match ? decodeURIComponent(match[1]) : '' } async function request(url: string, init: RequestInit = {}): Promise { const method = init.method ?? 'GET' const headers: Record = { ...(init.headers as Record) } if (method !== 'GET') { headers['X-XSRF-TOKEN'] = csrfToken() } const response = await fetch(url, { ...init, method, headers, credentials: 'same-origin' }) if (!response.ok) { const body = await response.text() throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`) } return response.json() as Promise } export function getOrgs(): Promise { return request('/api/orgs') } export function updateContact(id: number, contact: Contact): Promise { return request(`/api/orgs/${id}/contact`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(contact), }) } export function provisionChannels(id: number): Promise { return request(`/api/orgs/${id}/channels`, { method: 'POST' }) } export function uploadSeed(file: File): Promise { const form = new FormData() form.append('file', file) return request('/api/seed', { method: 'POST', body: form }) } export async function login(username: string, password: string): Promise { const body = new URLSearchParams({ username, password }) const response = await fetch('/api/auth/login', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-XSRF-TOKEN': csrfToken(), }, body, }) if (!response.ok) { throw new ApiError(response.status, '아이디 또는 비밀번호가 올바르지 않습니다.') } }