File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
export type OrgStatus = 'INFO_PENDING' | 'READY' | 'PARTIAL' | 'ACTIVE'
export type ContactCategory = 'APPLICANT' | 'MJ' | 'LAWYER' | 'OPERATOR' | 'ITN'
/**
* 공통코드 한 건. 화면 선택지는 소스가 아니라 DB에서 온다.
*
* code는 실제로 저장되는 값이고 label은 화면 표시명이다. 둘이 다를 수 있다 -
* 권리확인 세부항목은 저장값이 길고 화면에는 짧은 형태로 나온다.
* attrs에는 코드별 부가 속성이 들어간다(색 토큰, 쓰이는 자리, 추천값 등).
*/
export interface Code {
code: string
label: string
sortOrder: number
attrs: Record<string, unknown>
}
/** 그룹ID -> 코드 목록(정렬순, 사용 중인 것만). */
export type CodeMap = Record<string, Code[]>
/** 담당자관리에 등록된 담당자 1명. 신청기관 담당자는 affiliation에 소속 기관명이 들어간다. */
export interface Contact {
id: number
category: ContactCategory
name: string
affiliation: string | null
deptName: string | null
title: string | null
phone: string | null
email: string | null
}
/** 담당자 등록/수정 폼 입력. 폼 상태는 문자열로만 다루므로 빈 문자열이 "값 없음"을 뜻한다. */
export interface ContactInput {
category: ContactCategory
name: string
affiliation: string
deptName: string
title: string
phone: string
email: string
}
export interface Org {
id: number
orgNo: string
orgName: string
status: OrgStatus
applicant: Contact | null
mj: Contact | null
itn: Contact | null
lawyer: Contact | null
lawyerAssignedDate: string | null
channelIdMj: string | null
channelIdLaw: string | null
/** null이면 아직 어떤 진행단계도 시작되지 않은 것이다(채널 생성 전). 1..12. */
stage: number | null
}
/** 채널관리 화면이 신청기관/문정원/아이티앤/변호사 배정을 한 번에 바꿀 때 보내는 요청. null은 해제다. */
export interface AssignmentInput {
applicantContactId: number | null
mjContactId: number | null
itnContactId: number | null
lawyerContactId: number | null
lawyerAssignedDate: string | null
}
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
}
export interface FileRef {
id: string
name: string
size: number
}
export interface PostView {
id: string
user: string
message: string
createAt: number
system: boolean
files: FileRef[]
}
export interface FileView {
id: string
name: string
size: number
mimeType: string
createAt: number
uploader: string
}
export type ChannelKindKey = 'mj' | 'law'
export interface WorkMemo {
id: number
orgId: number
contactName: string
body: string
createdAt: number
}
/** 타임라인 탭 한 줄. detail은 STAGE 이벤트에서는 항상 null이다. */
export interface TimelineEvent {
type: 'STAGE' | 'MEMO' | 'FILE'
at: number
title: string
detail: string | null
}
/**
* 서버가 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<T>(url: string, init: RequestInit = {}): Promise<T> {
const method = init.method ?? 'GET'
const headers: Record<string, string> = { ...(init.headers as Record<string, string>) }
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<T>
}
export function getOrgs(): Promise<Org[]> {
return request<Org[]>('/api/orgs')
}
/** 모든 화면 선택지를 한 번에 받는다. 로그인 직후 1회만 부른다. */
export function getCodes(): Promise<CodeMap> {
return request<CodeMap>('/api/meta/codes')
}
/** 자료실에 올려둔 공용 서식(요구사항 [26]). 기관에 딸리지 않는다. */
export interface SharedFile {
id: number
fileName: string
description: string | null
byteSize: number
uploadedBy: string
uploadedAt: number
}
export function getSharedFiles(): Promise<SharedFile[]> {
return request<SharedFile[]>('/api/shared-files')
}
export async function addSharedFile(file: File, description: string): Promise<SharedFile[]> {
const form = new FormData()
form.append('file', file)
if (description) {
form.append('description', new Blob([description], { type: 'text/plain' }))
}
const res = await fetch('/api/shared-files', {
method: 'POST',
headers: { 'X-XSRF-TOKEN': csrfToken() },
credentials: 'same-origin',
body: form,
})
if (!res.ok) {
throw new ApiError(res.status, `자료 업로드 실패 (${res.status}): ${await res.text()}`)
}
return res.json() as Promise<SharedFile[]>
}
export function updateSharedFileDescription(id: number, description: string): Promise<SharedFile[]> {
return request<SharedFile[]>(`/api/shared-files/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description }),
})
}
export async function deleteSharedFile(id: number): Promise<void> {
await fetch(`/api/shared-files/${id}`, {
method: 'DELETE',
headers: { 'X-XSRF-TOKEN': csrfToken() },
credentials: 'same-origin',
})
}
export function sharedFileDownloadUrl(id: number): string {
return `/api/shared-files/${id}`
}
/** RE 메모 한 건. 요구사항 [4]대로 메모 1개가 RE 1개다. */
export interface ReMemo {
id: number
orgId: number
content: string
author: string
/** yyyy-MM-dd */
memoDate: string
resolved: boolean
createdAt: number
}
export interface ReMemoInput {
content: string
author: string
memoDate: string
resolved: boolean
}
export function getReMemos(orgId: number): Promise<ReMemo[]> {
return request<ReMemo[]>(`/api/orgs/${orgId}/re-memos`)
}
export function addReMemo(orgId: number, body: ReMemoInput): Promise<ReMemo[]> {
return request<ReMemo[]>(`/api/orgs/${orgId}/re-memos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export function updateReMemo(orgId: number, memoId: number, body: ReMemoInput): Promise<ReMemo[]> {
return request<ReMemo[]>(`/api/orgs/${orgId}/re-memos/${memoId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function deleteReMemo(orgId: number, memoId: number): Promise<void> {
await fetch(`/api/orgs/${orgId}/re-memos/${memoId}`, {
method: 'DELETE',
headers: { 'X-XSRF-TOKEN': csrfToken() },
credentials: 'same-origin',
})
}
export function updateAssignments(orgId: number, body: AssignmentInput): Promise<Org> {
return request<Org>(`/api/orgs/${orgId}/assignments`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export function getContacts(category?: ContactCategory): Promise<Contact[]> {
const query = category ? `?category=${category}` : ''
return request<Contact[]>(`/api/contacts${query}`)
}
export function createContact(body: ContactInput): Promise<Contact> {
return request<Contact>('/api/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export function updateContactInfo(id: number, body: ContactInput): Promise<Contact> {
return request<Contact>(`/api/contacts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다(deleteMemo()와 동일한 패턴). */
export async function deleteContact(id: number): Promise<void> {
const response = await fetch(`/api/contacts/${id}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
const body = await response.text()
throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`)
}
}
export function provisionChannels(id: number): Promise<ProvisionResult> {
return request<ProvisionResult>(`/api/orgs/${id}/channels`, { method: 'POST' })
}
export interface StageHistoryEntry {
stage: number
changedAt: number
}
/** 업무단계 스트립의 단계별 도달 날짜 표시용. 최신순으로 온다. */
export function getStageHistory(orgId: number): Promise<StageHistoryEntry[]> {
return request<StageHistoryEntry[]>(`/api/orgs/${orgId}/stage-history`)
}
export function updateStage(orgId: number, stage: number): Promise<Org> {
return request<Org>(`/api/orgs/${orgId}/stage`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stage }),
})
}
/** 대시보드 KPI 타일 7개. 모두 서버가 같은 원본에서 접어 만든 값이다. */
export interface DashboardSummary {
applied: number
docSubmitted: number
reviewTotal: number
reviewDone: number
processTotal: number
processDone: number
/** RE 데이터 모델이 아직 없어 항상 0이다. */
unresolvedRe: number
reportWriting: number
completed: number
}
/** 대시보드 칸반 카드 1장 = "최근 진행 기관" 표 1줄. 둘이 같은 행을 쓴다. */
export interface DashboardOrgRow {
id: number
orgNo: string
orgName: string
stage: number | null
/** false면 칸반의 "단계 미지정" 컬럼으로 간다. 드래그로 단계를 옮길 수 없다. */
hasChannel: boolean
lawyerName: string | null
mjName: string | null
lawyerAssignedDate: string | null
reviewTotal: number
reviewDone: number
processTotal: number
processDone: number
/** 현재 단계로 넘어온 시각(epoch ms). 카드의 D+n 계산용. */
stageChangedAt: number | null
/** 단계·권리확인/처리·메모·배정 중 가장 최근 변동 시각(epoch ms). */
lastChangedAt: number | null
reCount: number
}
export interface DashboardData {
summary: DashboardSummary
orgs: DashboardOrgRow[]
}
export function getDashboard(): Promise<DashboardData> {
return request<DashboardData>('/api/dashboard')
}
/** 사업관리 목록 1행. org는 대시보드와 같은 집계 행을 그대로 쓴다. */
export interface ProjectRow {
org: DashboardOrgRow
lastContactedOn: string | null
contactCount: number
reportCount: number
}
/** 신청 확인 연락 이력 1건. */
export interface ContactLog {
id: number
orgId: number
contactedOn: string
method: string
summary: string
author: string
createdAt: number
}
export interface ContactLogInput {
contactedOn: string
method: string
summary: string
}
/** 기관별 최종 보고서. */
export interface OrgReport {
id: number
orgId: number
fileName: string
contentType: string | null
byteSize: number
uploadedBy: string
uploadedAt: number
}
export function getProjects(): Promise<ProjectRow[]> {
return request<ProjectRow[]>('/api/projects')
}
export function getContactLogs(orgId: number): Promise<ContactLog[]> {
return request<ContactLog[]>(`/api/orgs/${orgId}/contact-logs`)
}
/** 등록하면 갱신된 이력 전체가 돌아온다(작성자는 서버가 기록). */
export function addContactLog(orgId: number, body: ContactLogInput): Promise<ContactLog[]> {
return request<ContactLog[]>(`/api/orgs/${orgId}/contact-logs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export function updateContactLog(
orgId: number,
logId: number,
body: ContactLogInput,
): Promise<ContactLog[]> {
return request<ContactLog[]>(`/api/orgs/${orgId}/contact-logs/${logId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export async function deleteContactLog(orgId: number, logId: number): Promise<void> {
const response = await fetch(`/api/orgs/${orgId}/contact-logs/${logId}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
throw new ApiError(response.status, `요청 실패 (${response.status})`)
}
}
export function getReports(orgId: number): Promise<OrgReport[]> {
return request<OrgReport[]>(`/api/orgs/${orgId}/reports`)
}
export function uploadReport(orgId: number, file: File): Promise<OrgReport[]> {
const form = new FormData()
form.append('file', file)
return request<OrgReport[]>(`/api/orgs/${orgId}/reports`, { method: 'POST', body: form })
}
/** 다운로드는 <a href>가 그대로 열게 둔다. */
export function reportDownloadUrl(orgId: number, reportId: number): string {
return `/api/orgs/${orgId}/reports/${reportId}`
}
export async function deleteReport(orgId: number, reportId: number): Promise<void> {
const response = await fetch(`/api/orgs/${orgId}/reports/${reportId}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
throw new ApiError(response.status, `요청 실패 (${response.status})`)
}
}
/** 시스템관리 > 채널 초기화 미리보기 1행. 웹앱 DB가 아는 채널만 담긴다. */
export interface ResetTarget {
orgId: number
orgNo: string
orgName: string
stage: number | null
channels: { kind: string; channelId: string }[]
}
export interface ResetReport {
archivedChannels: number
clearedOrgs: number
failures: string[]
}
/** 실행 확인 문구. 서버도 같은 값을 검사한다. */
export const RESET_CONFIRM_PHRASE = '채널 초기화'
export function getResetPreview(): Promise<ResetTarget[]> {
return request<ResetTarget[]>('/api/system/reset/preview')
}
export function resetChannels(confirm: string): Promise<ResetReport> {
return request<ResetReport>('/api/system/reset/channels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirm }),
})
}
export function getTimeline(orgId: number): Promise<TimelineEvent[]> {
return request<TimelineEvent[]>(`/api/orgs/${orgId}/timeline`)
}
export function getPosts(orgId: number, channel: ChannelKindKey): Promise<PostView[]> {
return request<PostView[]>(`/api/orgs/${orgId}/posts?channel=${channel}`)
}
export function getFiles(orgId: number, channel: ChannelKindKey): Promise<FileView[]> {
return request<FileView[]>(`/api/orgs/${orgId}/files?channel=${channel}`)
}
/** 다운로드는 <a href>가 그대로 열게 두므로 fetch()를 거치지 않는다. */
export function fileDownloadUrl(fileId: string): string {
return `/api/files/${fileId}`
}
export interface SendResult {
post: PostView
/** 게시글은 전송됐지만 공지 뒤처리(핀/헤더)가 일부 실패한 경우의 경고. 성공이면 null */
noticeWarning: string | null
}
export function sendMessage(
orgId: number,
channel: ChannelKindKey,
body: { message: string; notice: boolean; files: File[] },
): Promise<SendResult> {
const form = new FormData()
form.append('channel', channel)
form.append('message', body.message)
form.append('notice', String(body.notice))
for (const file of body.files) {
form.append('files', file)
}
return request<SendResult>(`/api/orgs/${orgId}/messages`, { method: 'POST', body: form })
}
/** 공지가 없으면 서버가 204를 주므로 request()의 json() 경로를 타지 않고 직접 부른다. */
export async function getNotice(orgId: number, channel: ChannelKindKey): Promise<PostView | null> {
const response = await fetch(`/api/orgs/${orgId}/notice?channel=${channel}`, {
credentials: 'same-origin',
})
if (response.status === 204) {
return null
}
if (!response.ok) {
throw new ApiError(response.status, `요청 실패 (${response.status})`)
}
return response.json() as Promise<PostView>
}
export async function clearNotice(orgId: number, channel: ChannelKindKey): Promise<void> {
const response = await fetch(`/api/orgs/${orgId}/notice?channel=${channel}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
throw new ApiError(response.status, `공지 해제 실패 (${response.status})`)
}
}
export function getMemos(orgId: number): Promise<WorkMemo[]> {
return request<WorkMemo[]>(`/api/orgs/${orgId}/memos`)
}
/** 작성자는 보내지 않는다 - 서버가 로그인 사용자로 기록한다. */
export function createMemo(orgId: number, memo: { body: string }): Promise<WorkMemo> {
return request<WorkMemo>(`/api/orgs/${orgId}/memos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(memo),
})
}
/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다(logout()과 동일한 패턴). */
/** 본문만 고친다. 작성자·작성시각은 서버가 그대로 유지한다. */
export function updateMemo(orgId: number, memoId: number, memo: { body: string }): Promise<WorkMemo> {
return request<WorkMemo>(`/api/orgs/${orgId}/memos/${memoId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(memo),
})
}
export async function deleteMemo(orgId: number, memoId: number): Promise<void> {
const response = await fetch(`/api/orgs/${orgId}/memos/${memoId}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
const body = await response.text()
throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`)
}
}
/** 권리확인 탭 게시물 한 건. W열은 원본 시트의 빈 열이라 대응 필드가 없다. */
export interface ReviewItem {
id: number
orgId: number
seq: number
siteName: string | null
category: string | null
boardPath: string | null
boardName: string | null
postTitle: string | null
url: string | null
postRegistered: string | null
producedDate: string | null
publishedDate: string | null
hasAttachment: string | null
koglAttached: string | null
koglType: string | null
aiType: string | null
surveyorNote: string | null
openable: string | null
reviewMajor: string | null
reviewMinor: string | null
reviewResult: string | null
judgedKoglType: string | null
judgedAiType: string | null
opinion: string | null
lawyerNote: string | null
needsProcessing: string | null
webEditedAt: number | null
createdAt: number
updatedAt: number
}
/** total은 현재 keyword 필터 기준 건수(페이지 계산용), done은 검색어와 무관한 기관 전체 처리 건수다. */
export interface ReviewPage {
items: ReviewItem[]
total: number
done: number
page: number
size: number
}
export interface ReviewImportReport {
created: number
updated: number
total: number
}
/** 권리확인 상세의 [수정] 저장 요청. 전부 선택값이다. */
export interface JudgmentRequest {
hasAttachment: string | null
koglAttached: string | null
koglType: string | null
aiType: string | null
openable: string | null
reviewMajor: string | null
reviewMinor: string | null
reviewResult: string | null
judgedKoglType: string | null
judgedAiType: string | null
opinion: string | null
lawyerNote: string | null
needsProcessing: string | null
}
export function importReview(orgId: number, file: File): Promise<ReviewImportReport> {
const form = new FormData()
form.append('file', file)
return request<ReviewImportReport>(`/api/orgs/${orgId}/review/import`, { method: 'POST', body: form })
}
/** 검색 조건. keyword는 통합검색, site/board/title은 열별 조건으로 서로 AND로 겹친다. */
export interface ItemSearch {
keyword?: string
site?: string
board?: string
title?: string
}
export function getReviewPage(
orgId: number,
params: ItemSearch & { page?: number; size?: number } = {},
): Promise<ReviewPage> {
const query = new URLSearchParams()
if (params.keyword) {
query.set('keyword', params.keyword)
}
for (const field of ['site', 'board', 'title'] as const) {
if (params[field]) {
query.set(field, params[field] as string)
}
}
if (params.page !== undefined) {
query.set('page', String(params.page))
}
if (params.size !== undefined) {
query.set('size', String(params.size))
}
const qs = query.toString()
return request<ReviewPage>(`/api/orgs/${orgId}/review${qs ? `?${qs}` : ''}`)
}
export function getReviewItem(orgId: number, itemId: number): Promise<ReviewItem> {
return request<ReviewItem>(`/api/orgs/${orgId}/review/${itemId}`)
}
export function updateReviewJudgment(
orgId: number,
itemId: number,
body: JudgmentRequest,
): Promise<ReviewItem> {
return request<ReviewItem>(`/api/orgs/${orgId}/review/${itemId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다. */
export async function deleteReviewItem(orgId: number, itemId: number): Promise<void> {
const response = await fetch(`/api/orgs/${orgId}/review/${itemId}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
const body = await response.text()
throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`)
}
}
/** 다운로드는 <a href>가 그대로 열게 두므로 fetch()를 거치지 않는다. */
export function reviewDownloadUrl(orgId: number): string {
return `/api/orgs/${orgId}/review/download`
}
/** 권리처리 탭 게시물 한 건. U(수정 링크)/Y(빈 열)열은 원본 시트의 건너뛰는 열이라 대응 필드가 없다. */
export interface ProcessItem {
id: number
orgId: number
seq: number
siteName: string | null
category: string | null
boardName: string | null
postTitle: string | null
url: string | null
description: string | null
hasAttachment: string | null
priorKoglType: string | null
contractDocs: string | null
producedDate: string | null
publishedDate: string | null
reviewMajor: string | null
reviewMinor: string | null
reviewResult: string | null
reviewKoglType: string | null
reviewAiType: string | null
reviewOpinion: string | null
reviewNote: string | null
priorEvidence: string | null
judgedKoglType: string | null
judgedAiType: string | null
finalOpinion: string | null
judgmentBasis: string | null
processStatus: string | null
webEditedAt: number | null
processedAt: number | null
createdAt: number
updatedAt: number
}
export type ProcessStatusFilter = 'DONE' | 'PENDING'
/** total/done은 검색어·상태 필터와 무관한 기관 전체 진행률이다(헤더 "총 n건 · 처리완료 m건"용). */
export interface ProcessPage {
items: ProcessItem[]
total: number
done: number
page: number
size: number
}
export interface ProcessImportReport {
created: number
updated: number
total: number
}
/** 권리처리 상세의 [처리등록]/[수정] 저장 요청. 전부 선택값이다. */
export interface ProcessingRequest {
contractDocs: string | null
judgedKoglType: string | null
judgedAiType: string | null
finalOpinion: string | null
judgmentBasis: string | null
processStatus: string | null
}
export function importProcess(orgId: number, file: File): Promise<ProcessImportReport> {
const form = new FormData()
form.append('file', file)
return request<ProcessImportReport>(`/api/orgs/${orgId}/process/import`, { method: 'POST', body: form })
}
/** 권리확인 일괄 등록. 비운 항목은 기존 값을 유지한다. */
export function bulkUpdateReview(
orgId: number,
body: {
ids: number[]
reviewResult?: string
judgedKoglType?: string
opinion?: string
/** 요구사항 [15]로 추가된 비고. */
lawyerNote?: string
},
): Promise<{ updated: number }> {
return request<{ updated: number }>(`/api/orgs/${orgId}/review/bulk`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export function bulkDeleteReview(orgId: number, ids: number[]): Promise<{ deleted: number }> {
return request<{ deleted: number }>(`/api/orgs/${orgId}/review/bulk-delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ids),
})
}
/** 권리처리 일괄 등록. 비운 항목은 기존 값을 유지한다. */
export function bulkUpdateProcess(
orgId: number,
body: {
ids: number[]
processStatus?: string
judgedKoglType?: string
finalOpinion?: string
/** 요구사항 [15]로 추가된 비고. */
reviewNote?: string
},
): Promise<{ updated: number }> {
return request<{ updated: number }>(`/api/orgs/${orgId}/process/bulk`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
export function bulkDeleteProcess(orgId: number, ids: number[]): Promise<{ deleted: number }> {
return request<{ deleted: number }>(`/api/orgs/${orgId}/process/bulk-delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ids),
})
}
export function getProcessPage(
orgId: number,
params: ItemSearch & { status?: ProcessStatusFilter; page?: number; size?: number } = {},
): Promise<ProcessPage> {
const query = new URLSearchParams()
if (params.keyword) {
query.set('keyword', params.keyword)
}
for (const field of ['site', 'board', 'title'] as const) {
if (params[field]) {
query.set(field, params[field] as string)
}
}
if (params.status) {
query.set('status', params.status)
}
if (params.page !== undefined) {
query.set('page', String(params.page))
}
if (params.size !== undefined) {
query.set('size', String(params.size))
}
const qs = query.toString()
return request<ProcessPage>(`/api/orgs/${orgId}/process${qs ? `?${qs}` : ''}`)
}
export function getProcessItem(orgId: number, itemId: number): Promise<ProcessItem> {
return request<ProcessItem>(`/api/orgs/${orgId}/process/${itemId}`)
}
export function updateProcessing(
orgId: number,
itemId: number,
body: ProcessingRequest,
): Promise<ProcessItem> {
return request<ProcessItem>(`/api/orgs/${orgId}/process/${itemId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}
/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다. */
export async function deleteProcessItem(orgId: number, itemId: number): Promise<void> {
const response = await fetch(`/api/orgs/${orgId}/process/${itemId}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
const body = await response.text()
throw new ApiError(response.status, `요청 실패 (${response.status}): ${body}`)
}
}
/** 다운로드는 <a href>가 그대로 열게 두므로 fetch()를 거치지 않는다. */
export function processDownloadUrl(orgId: number): string {
return `/api/orgs/${orgId}/process/download`
}
export function uploadSeed(file: File): Promise<SeedReport> {
const form = new FormData()
form.append('file', file)
return request<SeedReport>('/api/seed', { method: 'POST', body: form })
}
export interface MemberImportReport {
created: number
updated: number
skipped: number
assigned: number
}
export function importMembers(file: File): Promise<MemberImportReport> {
const form = new FormData()
form.append('file', file)
return request<MemberImportReport>('/api/contacts/import', { method: 'POST', body: form })
}
/** 204를 돌려주므로 request()의 json() 경로를 타지 않고 직접 부른다. */
export async function logout(): Promise<void> {
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-XSRF-TOKEN': csrfToken() },
})
if (!response.ok) {
throw new ApiError(response.status, `로그아웃 실패 (${response.status})`)
}
}
export async function login(username: string, password: string): Promise<void> {
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, '아이디 또는 비밀번호가 올바르지 않습니다.')
}
}