feat: fetchWrapper 마이그레이션 및 토큰 리프레시 캐싱 구현
- 40+ actions.ts 파일을 fetchWrapper 패턴으로 마이그레이션 - 토큰 리프레시 캐싱 로직 추가 (refresh-token.ts) - ApiErrorContext 추가로 전역 에러 처리 개선 - HR EmployeeForm 컴포넌트 개선 - 참조함(ReferenceBox) 기능 수정 - juil 테스트 URL 페이지 추가 - claudedocs 문서 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { BadDebtRecord, CollectionStatus } from './types';
|
||||
|
||||
// ============================================
|
||||
@@ -118,21 +118,6 @@ interface BadDebtSummaryApiData {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 상태 → 프론트엔드 상태 변환
|
||||
* API: legal_action, bad_debt (snake_case)
|
||||
@@ -267,7 +252,6 @@ export async function getBadDebts(params?: {
|
||||
client_id?: string;
|
||||
}): Promise<BadDebtRecord[]> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -281,14 +265,15 @@ export async function getBadDebts(params?: {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BadDebtActions] GET list error:', response.status);
|
||||
if (error) {
|
||||
console.error('[BadDebtActions] GET list error:', error.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[BadDebtActions] GET list error:', response?.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -311,19 +296,18 @@ export async function getBadDebts(params?: {
|
||||
*/
|
||||
export async function getBadDebtById(id: string): Promise<BadDebtRecord | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BadDebtActions] GET detail error:', response.status);
|
||||
if (error) {
|
||||
console.error('[BadDebtActions] GET detail error:', error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[BadDebtActions] GET detail error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -345,19 +329,18 @@ export async function getBadDebtById(id: string): Promise<BadDebtRecord | null>
|
||||
*/
|
||||
export async function getBadDebtSummary(): Promise<BadDebtSummaryApiData | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/summary`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BadDebtActions] GET summary error:', response.status);
|
||||
if (error) {
|
||||
console.error('[BadDebtActions] GET summary error:', error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[BadDebtActions] GET summary error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -381,26 +364,28 @@ export async function createBadDebt(
|
||||
data: Partial<BadDebtRecord>
|
||||
): Promise<{ success: boolean; data?: BadDebtRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[BadDebtActions] POST request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '악성채권 등록에 실패했습니다.',
|
||||
error: result?.message || '악성채권 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -427,26 +412,28 @@ export async function updateBadDebt(
|
||||
data: Partial<BadDebtRecord>
|
||||
): Promise<{ success: boolean; data?: BadDebtRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[BadDebtActions] PUT request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '악성채권 수정에 실패했습니다.',
|
||||
error: result?.message || '악성채권 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -470,22 +457,21 @@ export async function updateBadDebt(
|
||||
*/
|
||||
export async function deleteBadDebt(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '악성채권 삭제에 실패했습니다.',
|
||||
error: result?.message || '악성채권 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -506,22 +492,21 @@ export async function deleteBadDebt(id: string): Promise<{ success: boolean; err
|
||||
*/
|
||||
export async function toggleBadDebt(id: string): Promise<{ success: boolean; data?: BadDebtRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}/toggle`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
}
|
||||
{ method: 'PATCH' }
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '상태 변경에 실패했습니다.',
|
||||
error: result?.message || '상태 변경에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -548,23 +533,24 @@ export async function addBadDebtMemo(
|
||||
content: string
|
||||
): Promise<{ success: boolean; data?: { id: string; content: string; createdAt: string; createdBy: string }; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${badDebtId}/memos`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ content }),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '메모 추가에 실패했습니다.',
|
||||
error: result?.message || '메모 추가에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -595,22 +581,21 @@ export async function deleteBadDebtMemo(
|
||||
memoId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${badDebtId}/memos/${memoId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '메모 삭제에 실패했습니다.',
|
||||
error: result?.message || '메모 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -622,4 +607,4 @@ export async function deleteBadDebtMemo(
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { BankTransaction, TransactionKind } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface BankTransactionApiItem {
|
||||
id: number;
|
||||
@@ -101,7 +88,6 @@ export async function getBankTransactionList(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -117,19 +103,24 @@ export async function getBankTransactionList(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-transactions${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[BankTransactionActions] GET bank-transactions error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[BankTransactionActions] GET bank-transactions error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,7 +180,6 @@ export async function getBankTransactionSummary(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
@@ -198,17 +188,17 @@ export async function getBankTransactionSummary(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-transactions/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[BankTransactionActions] GET summary error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[BankTransactionActions] GET summary error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -248,21 +238,20 @@ export async function getBankAccountOptions(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-transactions/accounts`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[BankTransactionActions] GET accounts error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[BankTransactionActions] GET accounts error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -288,4 +277,4 @@ export async function getBankAccountOptions(): Promise<{
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,11 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { BillRecord, BillApiData, BillStatus } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 어음 목록 조회 =====
|
||||
export async function getBills(params: {
|
||||
search?: string;
|
||||
@@ -43,9 +31,9 @@ export async function getBills(params: {
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params.search) queryParams.append('search', params.search);
|
||||
@@ -62,11 +50,29 @@ export async function getBills(params: {
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage));
|
||||
if (params.page) queryParams.append('page', String(params.page));
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/bills?${queryParams.toString()}`,
|
||||
{ method: 'GET', headers, cache: 'no-store' }
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
__authError: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: error?.message || 'Failed to fetch bills',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -112,15 +118,22 @@ export async function getBill(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: BillRecord;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/bills/${id}`,
|
||||
{ method: 'GET', headers, cache: 'no-store' }
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || 'Failed to fetch bill' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -140,27 +153,32 @@ export async function getBill(id: string): Promise<{
|
||||
// ===== 어음 등록 =====
|
||||
export async function createBill(
|
||||
data: Partial<BillRecord>
|
||||
): Promise<{ success: boolean; data?: BillRecord; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: BillRecord; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[createBill] Sending data:', JSON.stringify(apiData, null, 2));
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/bills`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || 'Failed to create bill' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[createBill] Response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
// 유효성 검사 에러 처리
|
||||
if (result.errors) {
|
||||
const errorMessages = Object.values(result.errors).flat().join(', ');
|
||||
return { success: false, error: errorMessages || result.message || 'Failed to create bill' };
|
||||
@@ -182,27 +200,32 @@ export async function createBill(
|
||||
export async function updateBill(
|
||||
id: string,
|
||||
data: Partial<BillRecord>
|
||||
): Promise<{ success: boolean; data?: BillRecord; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: BillRecord; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[updateBill] Sending data:', JSON.stringify(apiData, null, 2));
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/bills/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || 'Failed to update bill' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[updateBill] Response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
// 유효성 검사 에러 처리
|
||||
if (result.errors) {
|
||||
const errorMessages = Object.values(result.errors).flat().join(', ');
|
||||
return { success: false, error: errorMessages || result.message || 'Failed to update bill' };
|
||||
@@ -221,15 +244,21 @@ export async function updateBill(
|
||||
}
|
||||
|
||||
// ===== 어음 삭제 =====
|
||||
export async function deleteBill(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteBill(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/bills/${id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || 'Failed to delete bill' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -247,19 +276,24 @@ export async function deleteBill(id: string): Promise<{ success: boolean; error?
|
||||
export async function updateBillStatus(
|
||||
id: string,
|
||||
status: BillStatus
|
||||
): Promise<{ success: boolean; data?: BillRecord; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: BillRecord; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/bills/${id}/status`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({ status }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || 'Failed to update bill status' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -293,9 +327,9 @@ export async function getBillSummary(params: {
|
||||
maturityAlertAmount: number;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params.billType && params.billType !== 'all') queryParams.append('bill_type', params.billType);
|
||||
@@ -304,11 +338,19 @@ export async function getBillSummary(params: {
|
||||
if (params.maturityStartDate) queryParams.append('maturity_start_date', params.maturityStartDate);
|
||||
if (params.maturityEndDate) queryParams.append('maturity_end_date', params.maturityEndDate);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/bills/summary?${queryParams.toString()}`,
|
||||
{ method: 'GET', headers, cache: 'no-store' }
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || 'Failed to fetch summary' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -336,15 +378,22 @@ export async function getClients(): Promise<{
|
||||
success: boolean;
|
||||
data?: { id: number; name: string }[];
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${API_URL}/api/v1/clients?per_page=100`,
|
||||
{ method: 'GET', headers, cache: 'no-store' }
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || 'Failed to fetch clients' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { CardTransaction } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface CardTransactionApiItem {
|
||||
id: number;
|
||||
@@ -105,7 +92,6 @@ export async function getCardTransactionList(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -120,19 +106,24 @@ export async function getCardTransactionList(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/card-transactions${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[CardTransactionActions] GET card-transactions error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[CardTransactionActions] GET card-transactions error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,7 +183,6 @@ export async function getCardTransactionSummary(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
@@ -201,17 +191,17 @@ export async function getCardTransactionSummary(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/card-transactions/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[CardTransactionActions] GET summary error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[CardTransactionActions] GET summary error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -254,20 +244,22 @@ export async function bulkUpdateAccountCode(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/card-transactions/bulk-update-account`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({ ids, account_code: accountCode }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[CardTransactionActions] PUT bulk-update-account error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[CardTransactionActions] PUT bulk-update-account error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -291,4 +283,4 @@ export async function bulkUpdateAccountCode(
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { NoteReceivableItem, DailyAccountItem, MatchStatus } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface NoteReceivableItemApi {
|
||||
id: string;
|
||||
@@ -89,7 +77,6 @@ export async function getNoteReceivables(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
@@ -97,18 +84,18 @@ export async function getNoteReceivables(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/daily-report/note-receivables${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[DailyReportActions] GET note-receivables error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[DailyReportActions] GET note-receivables error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,7 +134,6 @@ export async function getDailyAccounts(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
@@ -155,18 +141,18 @@ export async function getDailyAccounts(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/daily-report/daily-accounts${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[DailyReportActions] GET daily-accounts error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[DailyReportActions] GET daily-accounts error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -223,7 +209,6 @@ export async function getDailyReportSummary(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
@@ -231,17 +216,17 @@ export async function getDailyReportSummary(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/daily-report/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[DailyReportActions] GET summary error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[DailyReportActions] GET summary error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -331,4 +316,4 @@ export async function exportDailyReportExcel(params?: {
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { DepositRecord, DepositType, DepositStatus } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface DepositApiData {
|
||||
id: number;
|
||||
@@ -60,6 +47,23 @@ function transformApiToFrontend(apiData: DepositApiData): DepositRecord {
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Frontend → API 변환 =====
|
||||
function transformFrontendToApi(data: Partial<DepositRecord>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (data.depositDate !== undefined) result.deposit_date = data.depositDate;
|
||||
if (data.depositAmount !== undefined) result.deposit_amount = data.depositAmount;
|
||||
if (data.accountName !== undefined) result.account_name = data.accountName;
|
||||
if (data.depositorName !== undefined) result.depositor_name = data.depositorName;
|
||||
if (data.note !== undefined) result.note = data.note || null;
|
||||
if (data.depositType !== undefined) result.deposit_type = data.depositType;
|
||||
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
|
||||
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
|
||||
if (data.status !== undefined) result.status = data.status;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== 입금 내역 조회 =====
|
||||
export async function getDeposits(params?: {
|
||||
page?: number;
|
||||
@@ -80,122 +84,102 @@ export async function getDeposits(params?: {
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.depositType && params.depositType !== 'all') {
|
||||
searchParams.set('deposit_type', params.depositType);
|
||||
}
|
||||
if (params?.vendor && params.vendor !== 'all') {
|
||||
searchParams.set('vendor', params.vendor);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.depositType && params.depositType !== 'all') {
|
||||
searchParams.set('deposit_type', params.depositType);
|
||||
}
|
||||
if (params?.vendor && params.vendor !== 'all') {
|
||||
searchParams.set('vendor', params.vendor);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits${queryString ? `?${queryString}` : ''}`;
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[DepositActions] GET deposits error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '입금 내역 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// API 응답 구조 처리: { data: { data: [...], current_page: ... } } 또는 { data: [...], meta: {...} }
|
||||
const isPaginatedResponse = result.data && typeof result.data === 'object' && 'data' in result.data && Array.isArray(result.data.data);
|
||||
const rawData = isPaginatedResponse ? result.data.data : (Array.isArray(result.data) ? result.data : []);
|
||||
const deposits = rawData.map(transformApiToFrontend);
|
||||
|
||||
const meta: PaginationMeta = isPaginatedResponse
|
||||
? {
|
||||
current_page: result.data.current_page || 1,
|
||||
last_page: result.data.last_page || 1,
|
||||
per_page: result.data.per_page || 20,
|
||||
total: result.data.total || deposits.length,
|
||||
}
|
||||
: result.meta || {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: deposits.length,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deposits,
|
||||
pagination: {
|
||||
currentPage: meta.current_page,
|
||||
lastPage: meta.last_page,
|
||||
perPage: meta.per_page,
|
||||
total: meta.total,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[DepositActions] getDeposits error:', error);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[DepositActions] GET deposits error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '입금 내역 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// API 응답 구조 처리: { data: { data: [...], current_page: ... } } 또는 { data: [...], meta: {...} }
|
||||
const isPaginatedResponse = result.data && typeof result.data === 'object' && 'data' in result.data && Array.isArray(result.data.data);
|
||||
const rawData = isPaginatedResponse ? result.data.data : (Array.isArray(result.data) ? result.data : []);
|
||||
const deposits = rawData.map(transformApiToFrontend);
|
||||
|
||||
const meta: PaginationMeta = isPaginatedResponse
|
||||
? {
|
||||
current_page: result.data.current_page || 1,
|
||||
last_page: result.data.last_page || 1,
|
||||
per_page: result.data.per_page || 20,
|
||||
total: result.data.total || deposits.length,
|
||||
}
|
||||
: result.meta || {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: deposits.length,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deposits,
|
||||
pagination: {
|
||||
currentPage: meta.current_page,
|
||||
lastPage: meta.last_page,
|
||||
perPage: meta.per_page,
|
||||
total: meta.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 입금 내역 삭제 =====
|
||||
export async function deleteDeposit(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입금 내역 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[DepositActions] deleteDeposit error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '입금 내역 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===== 계정과목명 일괄 저장 =====
|
||||
@@ -203,38 +187,26 @@ export async function updateDepositTypes(
|
||||
ids: string[],
|
||||
depositType: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/bulk-update-type`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
deposit_type: depositType,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/bulk-update-type`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
deposit_type: depositType,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '계정과목명 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[DepositActions] updateDepositTypes error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '계정과목명 저장에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===== 입금 상세 조회 =====
|
||||
@@ -243,105 +215,52 @@ export async function getDepositById(id: string): Promise<{
|
||||
data?: DepositRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[DepositActions] GET deposit error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입금 내역 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[DepositActions] getDepositById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Frontend → API 변환 =====
|
||||
function transformFrontendToApi(data: Partial<DepositRecord>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
if (!response?.ok) {
|
||||
console.error('[DepositActions] GET deposit error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
if (data.depositDate !== undefined) result.deposit_date = data.depositDate;
|
||||
if (data.depositAmount !== undefined) result.deposit_amount = data.depositAmount;
|
||||
if (data.accountName !== undefined) result.account_name = data.accountName;
|
||||
if (data.depositorName !== undefined) result.depositor_name = data.depositorName;
|
||||
if (data.note !== undefined) result.note = data.note || null;
|
||||
if (data.depositType !== undefined) result.deposit_type = data.depositType;
|
||||
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
|
||||
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
|
||||
if (data.status !== undefined) result.status = data.status;
|
||||
const result = await response.json();
|
||||
|
||||
return result;
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, error: result.message || '입금 내역 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 입금 등록 =====
|
||||
export async function createDeposit(
|
||||
data: Partial<DepositRecord>
|
||||
): Promise<{ success: boolean; data?: DepositRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[DepositActions] POST deposit request:', apiData);
|
||||
|
||||
console.log('[DepositActions] POST deposit request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[DepositActions] POST deposit response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입금 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[DepositActions] createDeposit error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[DepositActions] POST deposit response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '입금 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 입금 수정 =====
|
||||
@@ -349,42 +268,27 @@ export async function updateDeposit(
|
||||
id: string,
|
||||
data: Partial<DepositRecord>
|
||||
): Promise<{ success: boolean; data?: DepositRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[DepositActions] PUT deposit request:', apiData);
|
||||
|
||||
console.log('[DepositActions] PUT deposit request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[DepositActions] PUT deposit response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입금 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[DepositActions] updateDeposit error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[DepositActions] PUT deposit response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '입금 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 거래처 목록 조회 =====
|
||||
@@ -393,39 +297,30 @@ export async function getVendors(): Promise<{
|
||||
data: { id: string; name: string }[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const clients = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: clients.map((c: { id: number; name: string }) => ({
|
||||
id: String(c.id),
|
||||
name: c.name,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[DepositActions] getVendors error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const clients = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: clients.map((c: { id: number; name: string }) => ({
|
||||
id: String(c.id),
|
||||
name: c.name,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface ExpectedExpenseApiData {
|
||||
id: number;
|
||||
@@ -126,7 +113,6 @@ export async function getExpectedExpenses(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -150,19 +136,24 @@ export async function getExpectedExpenses(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ExpectedExpenseActions] GET expected-expenses error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[ExpectedExpenseActions] GET expected-expenses error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,22 +205,20 @@ export async function getExpectedExpenseById(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[ExpectedExpenseActions] GET expected-expense error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[ExpectedExpenseActions] GET expected-expense error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -260,24 +249,26 @@ export async function createExpectedExpense(
|
||||
data: Partial<ExpectedExpenseRecord>
|
||||
): Promise<{ success: boolean; data?: ExpectedExpenseRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '미지급비용 등록에 실패했습니다.',
|
||||
error: result?.message || '미지급비용 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -300,24 +291,26 @@ export async function updateExpectedExpense(
|
||||
data: Partial<ExpectedExpenseRecord>
|
||||
): Promise<{ success: boolean; data?: ExpectedExpenseRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '미지급비용 수정에 실패했습니다.',
|
||||
error: result?.message || '미지급비용 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -337,22 +330,21 @@ export async function updateExpectedExpense(
|
||||
// ===== 미지급비용 삭제 =====
|
||||
export async function deleteExpectedExpense(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '미지급비용 삭제에 실패했습니다.',
|
||||
error: result?.message || '미지급비용 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -373,25 +365,26 @@ export async function deleteExpectedExpenses(ids: string[]): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '미지급비용 일괄 삭제에 실패했습니다.',
|
||||
error: result?.message || '미지급비용 일괄 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -418,13 +411,10 @@ export async function updateExpectedPaymentDate(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/update-payment-date`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
expected_payment_date: expectedPaymentDate,
|
||||
@@ -432,12 +422,16 @@ export async function updateExpectedPaymentDate(
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '예상 지급일 변경에 실패했습니다.',
|
||||
error: result?.message || '예상 지급일 변경에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -465,7 +459,6 @@ export async function getExpectedExpenseSummary(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
@@ -477,17 +470,17 @@ export async function getExpectedExpenseSummary(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ExpectedExpenseActions] GET summary error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[ExpectedExpenseActions] GET summary error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -520,19 +513,17 @@ export async function getClients(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -563,19 +554,17 @@ export async function getBankAccounts(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -599,4 +588,4 @@ export async function getBankAccounts(): Promise<{
|
||||
console.error('[ExpectedExpenseActions] getBankAccounts error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { PurchaseRecord, PurchaseType } from './types';
|
||||
|
||||
// ===== API 데이터 타입 =====
|
||||
@@ -92,19 +92,6 @@ function transformFrontendToApi(data: Partial<PurchaseRecord>): Record<string, u
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
@@ -128,81 +115,75 @@ export async function getPurchases(params?: {
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases${queryString ? `?${queryString}` : ''}`;
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('[PurchaseActions] GET purchases:', url);
|
||||
console.log('[PurchaseActions] GET purchases:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[PurchaseActions] GET purchases error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '매입 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const paginatedData: PurchaseApiPaginatedResponse = result.data || {
|
||||
data: [],
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const purchases = (paginatedData.data || []).map(transformApiToFrontend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: purchases,
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
perPage: paginatedData.per_page,
|
||||
total: paginatedData.total,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] getPurchases error:', error);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[PurchaseActions] GET purchases error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '매입 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const paginatedData: PurchaseApiPaginatedResponse = result.data || {
|
||||
data: [],
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const purchases = (paginatedData.data || []).map(transformApiToFrontend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: purchases,
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
perPage: paginatedData.per_page,
|
||||
total: paginatedData.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 매입 상세 조회 =====
|
||||
@@ -211,88 +192,52 @@ export async function getPurchaseById(id: string): Promise<{
|
||||
data?: PurchaseRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PurchaseActions] GET purchase error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매입 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] getPurchaseById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[PurchaseActions] GET purchase error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, error: result.message || '매입 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 매입 등록 =====
|
||||
export async function createPurchase(
|
||||
data: Partial<PurchaseRecord>
|
||||
): Promise<{ success: boolean; data?: PurchaseRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[PurchaseActions] POST purchase request:', apiData);
|
||||
|
||||
console.log('[PurchaseActions] POST purchase request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PurchaseActions] POST purchase response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매입 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] createPurchase error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[PurchaseActions] POST purchase response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매입 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 매입 수정 =====
|
||||
@@ -300,42 +245,27 @@ export async function updatePurchase(
|
||||
id: string,
|
||||
data: Partial<PurchaseRecord>
|
||||
): Promise<{ success: boolean; data?: PurchaseRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[PurchaseActions] PUT purchase request:', apiData);
|
||||
|
||||
console.log('[PurchaseActions] PUT purchase request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PurchaseActions] PUT purchase response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매입 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] updatePurchase error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[PurchaseActions] PUT purchase response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매입 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 세금계산서 수취 상태 토글 =====
|
||||
@@ -348,35 +278,21 @@ export async function togglePurchaseTaxInvoice(
|
||||
|
||||
// ===== 매입 삭제 =====
|
||||
export async function deletePurchase(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PurchaseActions] DELETE purchase response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매입 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] deletePurchase error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[PurchaseActions] DELETE purchase response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매입 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===== 매입 확정 =====
|
||||
@@ -385,38 +301,21 @@ export async function confirmPurchase(id: string): Promise<{
|
||||
data?: PurchaseRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}/confirm`;
|
||||
const { response, error } = await serverFetch(url, { method: 'PUT' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}/confirm`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PurchaseActions] PUT confirm response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매입 확정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] confirmPurchase error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[PurchaseActions] PUT confirm response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매입 확정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 은행 계좌 목록 조회 =====
|
||||
@@ -425,43 +324,34 @@ export async function getBankAccounts(): Promise<{
|
||||
data: { id: string; bankName: string; accountName: string; accountNumber: string }[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const accounts = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accounts.map((a: { id: number; bank_name: string; account_name: string; account_number: string }) => ({
|
||||
id: String(a.id),
|
||||
bankName: a.bank_name,
|
||||
accountName: a.account_name,
|
||||
accountNumber: a.account_number,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] getBankAccounts error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const accounts = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accounts.map((a: { id: number; bank_name: string; account_name: string; account_number: string }) => ({
|
||||
id: String(a.id),
|
||||
bankName: a.bank_name,
|
||||
accountName: a.account_name,
|
||||
accountNumber: a.account_number,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 거래처 목록 조회 =====
|
||||
@@ -470,39 +360,30 @@ export async function getVendors(): Promise<{
|
||||
data: { id: string; name: string }[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const clients = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: clients.map((c: { id: number; name: string }) => ({
|
||||
id: String(c.id),
|
||||
name: c.name,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PurchaseActions] getVendors error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const clients = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: clients.map((c: { id: number; name: string }) => ({
|
||||
id: String(c.id),
|
||||
name: c.name,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { VendorReceivables, CategoryType, MonthlyAmount } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface CategoryAmountApi {
|
||||
category: CategoryType;
|
||||
@@ -65,7 +53,6 @@ export async function getReceivablesList(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
@@ -77,18 +64,18 @@ export async function getReceivablesList(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivables${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ReceivablesActions] GET receivables error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[ReceivablesActions] GET receivables error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,7 +121,6 @@ export async function getReceivablesSummary(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
@@ -142,17 +128,17 @@ export async function getReceivablesSummary(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivables/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ReceivablesActions] GET summary error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[ReceivablesActions] GET summary error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,12 +182,10 @@ export async function updateOverdueStatus(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivables/overdue-status`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
updates: updates.map(item => ({
|
||||
id: item.id,
|
||||
@@ -210,11 +194,15 @@ export async function updateOverdueStatus(
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ReceivablesActions] PUT overdue-status error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[ReceivablesActions] PUT overdue-status error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -296,4 +284,4 @@ export async function exportReceivablesExcel(params?: {
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
SalesRecord,
|
||||
SaleApiData,
|
||||
@@ -21,19 +21,6 @@ import type {
|
||||
} from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
@@ -57,82 +44,76 @@ export async function getSales(params?: {
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales${queryString ? `?${queryString}` : ''}`;
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('[SalesActions] GET sales:', url);
|
||||
console.log('[SalesActions] GET sales:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[SalesActions] GET sales error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '매출 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// API 응답 구조: { success, data: { data: [], current_page, last_page, per_page, total } }
|
||||
const paginatedData: SaleApiPaginatedResponse = result.data || {
|
||||
data: [],
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const sales = (paginatedData.data || []).map(transformApiToFrontend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: sales,
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
perPage: paginatedData.per_page,
|
||||
total: paginatedData.total,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SalesActions] getSales error:', error);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[SalesActions] GET sales error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '매출 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// API 응답 구조: { success, data: { data: [], current_page, last_page, per_page, total } }
|
||||
const paginatedData: SaleApiPaginatedResponse = result.data || {
|
||||
data: [],
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const sales = (paginatedData.data || []).map(transformApiToFrontend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: sales,
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
perPage: paginatedData.per_page,
|
||||
total: paginatedData.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 매출 상세 조회 =====
|
||||
@@ -141,88 +122,52 @@ export async function getSaleById(id: string): Promise<{
|
||||
data?: SalesRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[SalesActions] GET sale error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매출 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SalesActions] getSaleById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[SalesActions] GET sale error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, error: result.message || '매출 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 매출 등록 =====
|
||||
export async function createSale(
|
||||
data: Partial<SalesRecord>
|
||||
): Promise<{ success: boolean; data?: SalesRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[SalesActions] POST sale request:', apiData);
|
||||
|
||||
console.log('[SalesActions] POST sale request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[SalesActions] POST sale response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매출 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SalesActions] createSale error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[SalesActions] POST sale response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매출 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 매출 수정 =====
|
||||
@@ -230,42 +175,27 @@ export async function updateSale(
|
||||
id: string,
|
||||
data: Partial<SalesRecord>
|
||||
): Promise<{ success: boolean; data?: SalesRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[SalesActions] PUT sale request:', apiData);
|
||||
|
||||
console.log('[SalesActions] PUT sale request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[SalesActions] PUT sale response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매출 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SalesActions] updateSale error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[SalesActions] PUT sale response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매출 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 세금계산서/거래명세서 발행 상태 토글 =====
|
||||
@@ -284,35 +214,21 @@ export async function toggleSaleIssuance(
|
||||
|
||||
// ===== 매출 삭제 =====
|
||||
export async function deleteSale(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[SalesActions] DELETE sale response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매출 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[SalesActions] deleteSale error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[SalesActions] DELETE sale response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매출 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===== 매출 확정 =====
|
||||
@@ -321,38 +237,21 @@ export async function confirmSale(id: string): Promise<{
|
||||
data?: SalesRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}/confirm`;
|
||||
const { response, error } = await serverFetch(url, { method: 'PUT' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}/confirm`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[SalesActions] PUT confirm response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매출 확정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SalesActions] confirmSale error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[SalesActions] PUT confirm response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '매출 확정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 매출 요약 통계 =====
|
||||
@@ -371,58 +270,43 @@ export async function getSalesSummary(params?: {
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/summary${queryString ? `?${queryString}` : ''}`;
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[SalesActions] GET summary error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '매출 요약 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
// API 응답 → 프론트엔드 형식 변환
|
||||
const summary = result.data || {};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalAmount: parseFloat(summary.total_amount) || 0,
|
||||
totalCount: summary.total_count || 0,
|
||||
confirmedAmount: parseFloat(summary.confirmed_amount) || 0,
|
||||
confirmedCount: summary.confirmed_count || 0,
|
||||
draftAmount: parseFloat(summary.draft_amount) || 0,
|
||||
draftCount: summary.draft_count || 0,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SalesActions] getSalesSummary error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[SalesActions] GET summary error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '매출 요약 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
// API 응답 → 프론트엔드 형식 변환
|
||||
const summary = result.data || {};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalAmount: parseFloat(summary.total_amount) || 0,
|
||||
totalCount: summary.total_count || 0,
|
||||
confirmedAmount: parseFloat(summary.confirmed_amount) || 0,
|
||||
confirmedCount: summary.confirmed_count || 0,
|
||||
draftAmount: parseFloat(summary.draft_amount) || 0,
|
||||
draftCount: summary.draft_count || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { VendorLedgerItem, VendorLedgerDetail, VendorLedgerSummary, TransactionEntry } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface VendorLedgerApiItem {
|
||||
id: number;
|
||||
@@ -171,7 +159,6 @@ export async function getVendorLedgerList(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -185,19 +172,24 @@ export async function getVendorLedgerList(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[VendorLedgerActions] GET vendor-ledger error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[VendorLedgerActions] GET vendor-ledger error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,7 +244,6 @@ export async function getVendorLedgerSummary(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
@@ -261,17 +252,17 @@ export async function getVendorLedgerSummary(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[VendorLedgerActions] GET summary error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[VendorLedgerActions] GET summary error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -315,7 +306,6 @@ export async function getVendorLedgerDetail(clientId: string, params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
@@ -324,17 +314,17 @@ export async function getVendorLedgerDetail(clientId: string, params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger/${clientId}${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[VendorLedgerActions] GET detail error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[VendorLedgerActions] GET detail error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -484,4 +474,4 @@ export async function exportVendorLedgerDetailPdf(clientId: string, params?: {
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
Vendor,
|
||||
ClientApiData,
|
||||
@@ -24,21 +24,6 @@ import type {
|
||||
BadDebtStatus,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 데이터 → 프론트엔드 타입 변환
|
||||
*/
|
||||
@@ -166,82 +151,59 @@ export async function getClients(params?: {
|
||||
q?: string;
|
||||
only_active?: boolean;
|
||||
}): Promise<{ success: boolean; data: Vendor[]; total: number; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.only_active !== undefined) searchParams.set('only_active', String(params.only_active));
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.only_active !== undefined) searchParams.set('only_active', String(params.only_active));
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?${searchParams.toString()}`;
|
||||
console.log('[VendorActions] GET clients:', url);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?${searchParams.toString()}`;
|
||||
console.log('[VendorActions] GET clients:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[VendorActions] GET clients error:', response.status);
|
||||
return { success: false, data: [], total: 0, error: `HTTP ${response.status}` };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<ClientApiData>> = await response.json();
|
||||
console.log('[VendorActions] GET clients response:', result.success, result.data?.total);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, data: [], total: 0, error: result.message };
|
||||
}
|
||||
|
||||
const vendors = result.data.data.map(transformApiToFrontend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: vendors,
|
||||
total: result.data.total,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[VendorActions] getClients error:', error);
|
||||
return { success: false, data: [], total: 0, error: '서버 오류가 발생했습니다.' };
|
||||
if (error) {
|
||||
return { success: false, data: [], total: 0, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[VendorActions] GET clients error:', response?.status);
|
||||
return { success: false, data: [], total: 0, error: `HTTP ${response?.status}` };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<ClientApiData>> = await response.json();
|
||||
console.log('[VendorActions] GET clients response:', result.success, result.data?.total);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, data: [], total: 0, error: result.message };
|
||||
}
|
||||
|
||||
const vendors = result.data.data.map(transformApiToFrontend);
|
||||
|
||||
return { success: true, data: vendors, total: result.data.total };
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 상세 조회
|
||||
*/
|
||||
export async function getClientById(id: string): Promise<Vendor | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[VendorActions] GET client error:', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<ClientApiData> = await response.json();
|
||||
console.log('[VendorActions] GET client response:', result.success);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
} catch (error) {
|
||||
console.error('[VendorActions] getClientById error:', error);
|
||||
if (error || !response?.ok) {
|
||||
console.error('[VendorActions] GET client error:', error?.message || response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<ClientApiData> = await response.json();
|
||||
console.log('[VendorActions] GET client response:', result.success);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,42 +212,27 @@ export async function getClientById(id: string): Promise<Vendor | null> {
|
||||
export async function createClient(
|
||||
data: Partial<Vendor>
|
||||
): Promise<{ success: boolean; data?: Vendor; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[VendorActions] POST client request:', apiData);
|
||||
|
||||
console.log('[VendorActions] POST client request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[VendorActions] POST client response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '거래처 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[VendorActions] createClient error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[VendorActions] POST client response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '거래처 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,113 +242,67 @@ export async function updateClient(
|
||||
id: string,
|
||||
data: Partial<Vendor>
|
||||
): Promise<{ success: boolean; data?: Vendor; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[VendorActions] PUT client request:', apiData);
|
||||
|
||||
console.log('[VendorActions] PUT client request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[VendorActions] PUT client response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '거래처 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[VendorActions] updateClient error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[VendorActions] PUT client response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '거래처 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 삭제
|
||||
*/
|
||||
export async function deleteClient(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[VendorActions] DELETE client response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '거래처 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[VendorActions] deleteClient error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[VendorActions] DELETE client response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '거래처 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 활성/비활성 토글
|
||||
*/
|
||||
export async function toggleClientActive(id: string): Promise<{ success: boolean; data?: Vendor; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}/toggle`;
|
||||
const { response, error } = await serverFetch(url, { method: 'PATCH' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}/toggle`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[VendorActions] PATCH toggle response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '상태 변경에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[VendorActions] toggleClientActive error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[VendorActions] PATCH toggle response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { WithdrawalRecord, WithdrawalType } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface WithdrawalApiData {
|
||||
id: number;
|
||||
@@ -58,6 +45,22 @@ function transformApiToFrontend(apiData: WithdrawalApiData): WithdrawalRecord {
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Frontend → API 변환 =====
|
||||
function transformFrontendToApi(data: Partial<WithdrawalRecord>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (data.withdrawalDate !== undefined) result.withdrawal_date = data.withdrawalDate;
|
||||
if (data.withdrawalAmount !== undefined) result.withdrawal_amount = data.withdrawalAmount;
|
||||
if (data.accountName !== undefined) result.account_name = data.accountName;
|
||||
if (data.recipientName !== undefined) result.recipient_name = data.recipientName;
|
||||
if (data.note !== undefined) result.note = data.note || null;
|
||||
if (data.withdrawalType !== undefined) result.withdrawal_type = data.withdrawalType;
|
||||
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
|
||||
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== 출금 내역 조회 =====
|
||||
export async function getWithdrawals(params?: {
|
||||
page?: number;
|
||||
@@ -78,111 +81,91 @@ export async function getWithdrawals(params?: {
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.withdrawalType && params.withdrawalType !== 'all') {
|
||||
searchParams.set('withdrawal_type', params.withdrawalType);
|
||||
}
|
||||
if (params?.vendor && params.vendor !== 'all') {
|
||||
searchParams.set('vendor', params.vendor);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.withdrawalType && params.withdrawalType !== 'all') {
|
||||
searchParams.set('withdrawal_type', params.withdrawalType);
|
||||
}
|
||||
if (params?.vendor && params.vendor !== 'all') {
|
||||
searchParams.set('vendor', params.vendor);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals${queryString ? `?${queryString}` : ''}`;
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WithdrawalActions] GET withdrawals error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '출금 내역 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const withdrawals = (result.data || []).map(transformApiToFrontend);
|
||||
const meta: PaginationMeta = result.meta || {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: withdrawals.length,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: withdrawals,
|
||||
pagination: {
|
||||
currentPage: meta.current_page,
|
||||
lastPage: meta.last_page,
|
||||
perPage: meta.per_page,
|
||||
total: meta.total,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WithdrawalActions] getWithdrawals error:', error);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[WithdrawalActions] GET withdrawals error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: result.message || '출금 내역 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const withdrawals = (result.data || []).map(transformApiToFrontend);
|
||||
const meta: PaginationMeta = result.meta || {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
total: withdrawals.length,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: withdrawals,
|
||||
pagination: {
|
||||
currentPage: meta.current_page,
|
||||
lastPage: meta.last_page,
|
||||
perPage: meta.per_page,
|
||||
total: meta.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 출금 내역 삭제 =====
|
||||
export async function deleteWithdrawal(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출금 내역 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[WithdrawalActions] deleteWithdrawal error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '출금 내역 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===== 계정과목명 일괄 저장 =====
|
||||
@@ -190,38 +173,26 @@ export async function updateWithdrawalTypes(
|
||||
ids: string[],
|
||||
withdrawalType: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/bulk-update-type`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
withdrawal_type: withdrawalType,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/bulk-update-type`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
withdrawal_type: withdrawalType,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '계정과목명 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[WithdrawalActions] updateWithdrawalTypes error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '계정과목명 저장에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ===== 출금 상세 조회 =====
|
||||
@@ -230,104 +201,52 @@ export async function getWithdrawalById(id: string): Promise<{
|
||||
data?: WithdrawalRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[WithdrawalActions] GET withdrawal error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출금 내역 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WithdrawalActions] getWithdrawalById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Frontend → API 변환 =====
|
||||
function transformFrontendToApi(data: Partial<WithdrawalRecord>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
if (!response?.ok) {
|
||||
console.error('[WithdrawalActions] GET withdrawal error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
if (data.withdrawalDate !== undefined) result.withdrawal_date = data.withdrawalDate;
|
||||
if (data.withdrawalAmount !== undefined) result.withdrawal_amount = data.withdrawalAmount;
|
||||
if (data.accountName !== undefined) result.account_name = data.accountName;
|
||||
if (data.recipientName !== undefined) result.recipient_name = data.recipientName;
|
||||
if (data.note !== undefined) result.note = data.note || null;
|
||||
if (data.withdrawalType !== undefined) result.withdrawal_type = data.withdrawalType;
|
||||
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
|
||||
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
|
||||
const result = await response.json();
|
||||
|
||||
return result;
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, error: result.message || '출금 내역 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 출금 등록 =====
|
||||
export async function createWithdrawal(
|
||||
data: Partial<WithdrawalRecord>
|
||||
): Promise<{ success: boolean; data?: WithdrawalRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[WithdrawalActions] POST withdrawal request:', apiData);
|
||||
|
||||
console.log('[WithdrawalActions] POST withdrawal request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WithdrawalActions] POST withdrawal response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출금 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WithdrawalActions] createWithdrawal error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[WithdrawalActions] POST withdrawal response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '출금 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 출금 수정 =====
|
||||
@@ -335,42 +254,27 @@ export async function updateWithdrawal(
|
||||
id: string,
|
||||
data: Partial<WithdrawalRecord>
|
||||
): Promise<{ success: boolean; data?: WithdrawalRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[WithdrawalActions] PUT withdrawal request:', apiData);
|
||||
|
||||
console.log('[WithdrawalActions] PUT withdrawal request:', apiData);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WithdrawalActions] PUT withdrawal response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출금 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WithdrawalActions] updateWithdrawal error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
console.log('[WithdrawalActions] PUT withdrawal response:', result);
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return { success: false, error: result?.message || '출금 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
}
|
||||
|
||||
// ===== 거래처 목록 조회 =====
|
||||
@@ -379,39 +283,30 @@ export async function getVendors(): Promise<{
|
||||
data: { id: string; name: string }[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const clients = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: clients.map((c: { id: number; name: string }) => ({
|
||||
id: String(c.id),
|
||||
name: c.name,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WithdrawalActions] getVendors error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const clients = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: clients.map((c: { id: number; name: string }) => ({
|
||||
id: String(c.id),
|
||||
name: c.name,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { ApprovalRecord, ApprovalType, ApprovalStatus } from './types';
|
||||
|
||||
// ============================================
|
||||
@@ -81,21 +81,6 @@ interface InboxStepApiData {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 상태 → 프론트엔드 상태 변환
|
||||
*/
|
||||
@@ -108,6 +93,22 @@ function mapApiStatus(apiStatus: string): ApprovalStatus {
|
||||
return statusMap[apiStatus] || 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트엔드 탭 상태 → 백엔드 API 상태 변환
|
||||
* 백엔드 inbox API가 기대하는 값:
|
||||
* - requested: 결재 요청 (현재 내 차례) = 미결재
|
||||
* - completed: 내가 처리 완료 = 결재완료
|
||||
* - rejected: 내가 반려한 문서 = 결재반려
|
||||
*/
|
||||
function mapTabToApiStatus(tabStatus: string): string | undefined {
|
||||
const statusMap: Record<string, string> = {
|
||||
'pending': 'requested', // 미결재 → 결재 요청
|
||||
'approved': 'completed', // 결재완료 → 처리 완료
|
||||
'rejected': 'rejected', // 반려 (동일)
|
||||
};
|
||||
return statusMap[tabStatus];
|
||||
}
|
||||
|
||||
/**
|
||||
* 양식 카테고리 → 결재 유형 변환
|
||||
*/
|
||||
@@ -175,16 +176,19 @@ export async function getInbox(params?: {
|
||||
approval_type?: string;
|
||||
sort_by?: string;
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: ApprovalRecord[]; total: number; lastPage: number }> {
|
||||
}): Promise<{ data: ApprovalRecord[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
// 프론트엔드 탭 상태를 백엔드 API 상태로 변환
|
||||
const apiStatus = mapTabToApiStatus(params.status);
|
||||
if (apiStatus) {
|
||||
searchParams.set('status', apiStatus);
|
||||
}
|
||||
}
|
||||
if (params?.approval_type && params.approval_type !== 'all') {
|
||||
searchParams.set('approval_type', params.approval_type);
|
||||
@@ -194,20 +198,20 @@ export async function getInbox(params?: {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/inbox?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[ApprovalBoxActions] GET inbox error:', response.status);
|
||||
if (error?.__authError) {
|
||||
return { data: [], total: 0, lastPage: 1, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
console.error('[ApprovalBoxActions] GET inbox error:', error?.message);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<InboxApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
if (!response.ok || !result.success || !result.data?.data) {
|
||||
console.warn('[ApprovalBoxActions] No data in response');
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
@@ -228,25 +232,19 @@ export async function getInbox(params?: {
|
||||
*/
|
||||
export async function getInboxSummary(): Promise<InboxSummary | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/inbox/summary`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[ApprovalBoxActions] GET inbox/summary error:', response.status);
|
||||
if (error?.__authError || !response) {
|
||||
console.error('[ApprovalBoxActions] GET inbox/summary error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<InboxSummary> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -260,19 +258,24 @@ export async function getInboxSummary(): Promise<InboxSummary | null> {
|
||||
/**
|
||||
* 승인 처리
|
||||
*/
|
||||
export async function approveDocument(id: string, comment?: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function approveDocument(id: string, comment?: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/approve`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ comment: comment || '' }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '승인 처리에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -295,7 +298,7 @@ export async function approveDocument(id: string, comment?: string): Promise<{ s
|
||||
/**
|
||||
* 반려 처리
|
||||
*/
|
||||
export async function rejectDocument(id: string, comment: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function rejectDocument(id: string, comment: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
if (!comment?.trim()) {
|
||||
return {
|
||||
@@ -304,17 +307,22 @@ export async function rejectDocument(id: string, comment: string): Promise<{ suc
|
||||
};
|
||||
}
|
||||
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/reject`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ comment }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '반려 처리에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -68,24 +68,23 @@ export function ApprovalLineSection({ data, onChange }: ApprovalLineSectionProps
|
||||
</div>
|
||||
) : (
|
||||
data.map((person, index) => (
|
||||
<div key={person.id} className="flex items-center gap-2">
|
||||
<div key={`${person.id}-${index}`} className="flex items-center gap-2">
|
||||
<span className="w-8 text-center text-sm text-gray-500">{index + 1}</span>
|
||||
<Select
|
||||
value={person.id.startsWith('temp-') ? '' : person.id}
|
||||
value={person.id.startsWith('temp-') ? undefined : person.id}
|
||||
onValueChange={(value) => handleChange(index, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
{/* 이미 선택된 값이 있으면 직접 표시, 없으면 placeholder */}
|
||||
{person.name && !person.id.startsWith('temp-') ? (
|
||||
<span>{person.department} / {person.position} / {person.name}</span>
|
||||
) : (
|
||||
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼" />
|
||||
)}
|
||||
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼">
|
||||
{person.name && !person.id.startsWith('temp-')
|
||||
? `${person.department || ''} / ${person.position || ''} / ${person.name}`
|
||||
: null}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{employees.map((employee) => (
|
||||
<SelectItem key={employee.id} value={employee.id}>
|
||||
{employee.department} / {employee.position} / {employee.name}
|
||||
{employee.department || ''} / {employee.position || ''} / {employee.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -68,24 +68,23 @@ export function ReferenceSection({ data, onChange }: ReferenceSectionProps) {
|
||||
</div>
|
||||
) : (
|
||||
data.map((person, index) => (
|
||||
<div key={person.id} className="flex items-center gap-2">
|
||||
<div key={`${person.id}-${index}`} className="flex items-center gap-2">
|
||||
<span className="w-8 text-center text-sm text-gray-500">{index + 1}</span>
|
||||
<Select
|
||||
value={person.id.startsWith('temp-') ? '' : person.id}
|
||||
value={person.id.startsWith('temp-') ? undefined : person.id}
|
||||
onValueChange={(value) => handleChange(index, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
{/* 이미 선택된 값이 있으면 직접 표시, 없으면 placeholder */}
|
||||
{person.name && !person.id.startsWith('temp-') ? (
|
||||
<span>{person.department} / {person.position} / {person.name}</span>
|
||||
) : (
|
||||
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼" />
|
||||
)}
|
||||
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼">
|
||||
{person.name && !person.id.startsWith('temp-')
|
||||
? `${person.department || ''} / ${person.position || ''} / ${person.name}`
|
||||
: null}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{employees.map((employee) => (
|
||||
<SelectItem key={employee.id} value={employee.id}>
|
||||
{employee.department} / {employee.position} / {employee.name}
|
||||
{employee.department || ''} / {employee.position || ''} / {employee.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
ExpenseEstimateItem,
|
||||
ApprovalPerson,
|
||||
@@ -74,21 +75,6 @@ interface ApprovalCreateResponse {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 비용견적서 API 데이터 → 프론트엔드 데이터 변환
|
||||
*/
|
||||
@@ -141,6 +127,8 @@ function transformEmployee(employee: EmployeeApiData): ApprovalPerson {
|
||||
* 파일 업로드
|
||||
* @param files 업로드할 파일 배열
|
||||
* @returns 업로드된 파일 정보 배열
|
||||
*
|
||||
* NOTE: 파일 업로드는 multipart/form-data가 필요하므로 serverFetch 대신 직접 fetch 사용
|
||||
*/
|
||||
export async function uploadFiles(files: File[]): Promise<{
|
||||
success: boolean;
|
||||
@@ -210,7 +198,6 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
|
||||
finalDifference: number;
|
||||
} | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (yearMonth) {
|
||||
@@ -219,12 +206,15 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/reports/expense-estimate?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[DocumentCreateActions] GET expense-estimate error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[DocumentCreateActions] GET expense-estimate error:', response.status);
|
||||
return null;
|
||||
@@ -254,7 +244,6 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
|
||||
*/
|
||||
export async function getEmployees(search?: string): Promise<ApprovalPerson[]> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('per_page', '100');
|
||||
if (search) {
|
||||
@@ -263,12 +252,15 @@ export async function getEmployees(search?: string): Promise<ApprovalPerson[]> {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[DocumentCreateActions] GET employees error:', error?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[DocumentCreateActions] GET employees error:', response.status);
|
||||
return [];
|
||||
@@ -296,8 +288,6 @@ export async function createApproval(formData: DocumentFormData): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
// 새 첨부파일 업로드
|
||||
const newFiles = formData.proposalData?.attachments
|
||||
|| formData.expenseReportData?.attachments
|
||||
@@ -332,15 +322,21 @@ export async function createApproval(formData: DocumentFormData): Promise<{
|
||||
content: getDocumentContent(formData, uploadedFiles),
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '문서 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApprovalCreateResponse> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -374,17 +370,21 @@ export async function submitApproval(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/submit`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '문서 상신에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -450,17 +450,17 @@ export async function getApprovalById(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '문서 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return { success: false, error: '문서를 찾을 수 없습니다.' };
|
||||
@@ -476,9 +476,9 @@ export async function getApprovalById(id: number): Promise<{
|
||||
|
||||
// API 응답을 프론트엔드 형식으로 변환
|
||||
const apiData = result.data;
|
||||
const formData = transformApiToFormData(apiData);
|
||||
const formDataResult = transformApiToFormData(apiData);
|
||||
|
||||
return { success: true, data: formData };
|
||||
return { success: true, data: formDataResult };
|
||||
} catch (error) {
|
||||
console.error('[DocumentCreateActions] getApprovalById error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
@@ -494,8 +494,6 @@ export async function updateApproval(id: number, formData: DocumentFormData): Pr
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
// 새 첨부파일 업로드
|
||||
const newFiles = formData.proposalData?.attachments
|
||||
|| formData.expenseReportData?.attachments
|
||||
@@ -528,15 +526,21 @@ export async function updateApproval(id: number, formData: DocumentFormData): Pr
|
||||
content: getDocumentContent(formData, uploadedFiles),
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '문서 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -601,16 +605,20 @@ export async function deleteApproval(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '문서 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { DraftRecord, DocumentStatus, Approver } from './types';
|
||||
|
||||
// ============================================
|
||||
@@ -85,21 +85,6 @@ interface ApprovalStepApiData {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 상태 → 프론트엔드 상태 변환
|
||||
*/
|
||||
@@ -193,9 +178,8 @@ export async function getDrafts(params?: {
|
||||
status?: string;
|
||||
sort_by?: string;
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: DraftRecord[]; total: number; lastPage: number }> {
|
||||
}): Promise<{ data: DraftRecord[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -217,27 +201,20 @@ export async function getDrafts(params?: {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/drafts?${searchParams.toString()}`;
|
||||
|
||||
console.log('[DraftBoxActions] Fetching:', url);
|
||||
console.log('[DraftBoxActions] Headers:', JSON.stringify(headers, null, 2));
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (error?.__authError) {
|
||||
return { data: [], total: 0, lastPage: 1, __authError: true };
|
||||
}
|
||||
|
||||
console.log('[DraftBoxActions] Response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('[DraftBoxActions] GET drafts error:', response.status, errorText);
|
||||
if (!response) {
|
||||
console.error('[DraftBoxActions] GET drafts error:', error?.message);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<ApprovalApiData>> = await response.json();
|
||||
console.log('[DraftBoxActions] Result:', JSON.stringify(result, null, 2).slice(0, 500));
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
if (!response.ok || !result.success || !result.data?.data) {
|
||||
console.warn('[DraftBoxActions] No data in response');
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
@@ -258,25 +235,19 @@ export async function getDrafts(params?: {
|
||||
*/
|
||||
export async function getDraftsSummary(): Promise<DraftsSummary | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/drafts/summary`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[DraftBoxActions] GET summary error:', response.status);
|
||||
if (error?.__authError || !response) {
|
||||
console.error('[DraftBoxActions] GET summary error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<DraftsSummary> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -292,25 +263,19 @@ export async function getDraftsSummary(): Promise<DraftsSummary | null> {
|
||||
*/
|
||||
export async function getDraftById(id: string): Promise<DraftRecord | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[DraftBoxActions] GET draft error:', response.status);
|
||||
if (error?.__authError || !response) {
|
||||
console.error('[DraftBoxActions] GET draft error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApprovalApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -324,18 +289,21 @@ export async function getDraftById(id: string): Promise<DraftRecord | null> {
|
||||
/**
|
||||
* 결재 문서 삭제 (임시저장 상태만)
|
||||
*/
|
||||
export async function deleteDraft(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteDraft(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '결재 문서 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -382,19 +350,24 @@ export async function deleteDrafts(ids: string[]): Promise<{ success: boolean; f
|
||||
/**
|
||||
* 결재 상신
|
||||
*/
|
||||
export async function submitDraft(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function submitDraft(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/submit`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '결재 상신에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -441,19 +414,24 @@ export async function submitDrafts(ids: string[]): Promise<{ success: boolean; f
|
||||
/**
|
||||
* 결재 회수 (기안자만)
|
||||
*/
|
||||
export async function cancelDraft(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function cancelDraft(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/cancel`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '결재 회수에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import type { ReferenceRecord, ApprovalType, DocumentStatus, ReadStatus } from './types';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { ReferenceRecord, ApprovalType, DocumentStatus } from './types';
|
||||
|
||||
// ============================================
|
||||
// API 응답 타입 정의
|
||||
@@ -72,21 +72,6 @@ interface ReferenceStepApiData {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 상태 → 프론트엔드 상태 변환
|
||||
*/
|
||||
@@ -156,7 +141,6 @@ export async function getReferences(params?: {
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: ReferenceRecord[]; total: number; lastPage: number }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -173,12 +157,16 @@ export async function getReferences(params?: {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/reference?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
// serverFetch handles 401 with redirect, so we just check for other errors
|
||||
if (error || !response) {
|
||||
console.error('[ReferenceBoxActions] GET reference error:', error?.message);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[ReferenceBoxActions] GET reference error:', response.status);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
@@ -228,16 +216,20 @@ export async function getReferenceSummary(): Promise<{ all: number; read: number
|
||||
*/
|
||||
export async function markAsRead(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/read`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/read`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
// serverFetch handles 401 with redirect
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '열람 처리에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
@@ -263,16 +255,20 @@ export async function markAsRead(id: string): Promise<{ success: boolean; error?
|
||||
*/
|
||||
export async function markAsUnread(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/unread`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/unread`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
// serverFetch handles 401 with redirect
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '미열람 처리에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Files,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Check,
|
||||
BookOpen,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -44,19 +43,22 @@ import {
|
||||
} from '@/components/templates/IntegratedListTemplateV2';
|
||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { DocumentDetailModal } from '@/components/approval/DocumentDetail';
|
||||
import type { DocumentType, ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData } from '@/components/approval/DocumentDetail/types';
|
||||
import type {
|
||||
ReferenceTabType,
|
||||
ReferenceRecord,
|
||||
SortOption,
|
||||
FilterOption,
|
||||
ApprovalType,
|
||||
} from './types';
|
||||
import {
|
||||
REFERENCE_TAB_LABELS,
|
||||
SORT_OPTIONS,
|
||||
FILTER_OPTIONS,
|
||||
APPROVAL_TYPE_LABELS,
|
||||
DOCUMENT_STATUS_LABELS,
|
||||
DOCUMENT_STATUS_COLORS,
|
||||
READ_STATUS_LABELS,
|
||||
READ_STATUS_COLORS,
|
||||
} from './types';
|
||||
|
||||
// ===== 통계 타입 =====
|
||||
@@ -86,6 +88,10 @@ export function ReferenceBox() {
|
||||
const [markReadDialogOpen, setMarkReadDialogOpen] = useState(false);
|
||||
const [markUnreadDialogOpen, setMarkUnreadDialogOpen] = useState(false);
|
||||
|
||||
// ===== 문서 상세 모달 상태 =====
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedDocument, setSelectedDocument] = useState<ReferenceRecord | null>(null);
|
||||
|
||||
// API 데이터
|
||||
const [data, setData] = useState<ReferenceRecord[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
@@ -252,6 +258,87 @@ export function ReferenceBox() {
|
||||
setMarkUnreadDialogOpen(false);
|
||||
}, [selectedItems, loadData, loadSummary]);
|
||||
|
||||
// ===== 문서 클릭/상세 보기 핸들러 =====
|
||||
const handleDocumentClick = useCallback((item: ReferenceRecord) => {
|
||||
setSelectedDocument(item);
|
||||
setIsModalOpen(true);
|
||||
}, []);
|
||||
|
||||
// ===== ApprovalType → DocumentType 변환 =====
|
||||
const getDocumentType = (approvalType: ApprovalType): DocumentType => {
|
||||
switch (approvalType) {
|
||||
case 'expense_estimate': return 'expenseEstimate';
|
||||
case 'expense_report': return 'expenseReport';
|
||||
default: return 'proposal';
|
||||
}
|
||||
};
|
||||
|
||||
// ===== ReferenceRecord → 모달용 데이터 변환 =====
|
||||
const convertToModalData = (item: ReferenceRecord): ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData => {
|
||||
const docType = getDocumentType(item.approvalType);
|
||||
const drafter = {
|
||||
id: 'drafter-1',
|
||||
name: item.drafter,
|
||||
position: item.drafterPosition,
|
||||
department: item.drafterDepartment,
|
||||
status: 'approved' as const,
|
||||
};
|
||||
const approvers = [{
|
||||
id: 'approver-1',
|
||||
name: '결재자',
|
||||
position: '부장',
|
||||
department: '경영지원팀',
|
||||
status: 'approved' as const,
|
||||
}];
|
||||
|
||||
switch (docType) {
|
||||
case 'expenseEstimate':
|
||||
return {
|
||||
documentNo: item.documentNo,
|
||||
createdAt: item.draftDate,
|
||||
items: [
|
||||
{ id: '1', expectedPaymentDate: '2025-11-05', category: '통신 서비스', amount: 550000, vendor: 'KT', account: '국민 123-456-789012 홍길동' },
|
||||
{ id: '2', expectedPaymentDate: '2025-11-12', category: '인건 대행', amount: 2500000, vendor: '에이치알코리아', account: '신한 110-123-456789 (주)에이치알' },
|
||||
],
|
||||
totalExpense: 3050000,
|
||||
accountBalance: 25000000,
|
||||
finalDifference: 21950000,
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
case 'expenseReport':
|
||||
return {
|
||||
documentNo: item.documentNo,
|
||||
createdAt: item.draftDate,
|
||||
requestDate: item.draftDate,
|
||||
paymentDate: item.draftDate,
|
||||
items: [
|
||||
{ id: '1', no: 1, description: '업무용 택시비', amount: 50000, note: '고객사 미팅' },
|
||||
{ id: '2', no: 2, description: '식대', amount: 30000, note: '팀 회식' },
|
||||
],
|
||||
cardInfo: '삼성카드 **** 1234',
|
||||
totalAmount: 80000,
|
||||
attachments: [],
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
documentNo: item.documentNo,
|
||||
createdAt: item.draftDate,
|
||||
vendor: '거래처',
|
||||
vendorPaymentDate: item.draftDate,
|
||||
title: item.title,
|
||||
description: item.title,
|
||||
reason: '업무상 필요',
|
||||
estimatedCost: 1000000,
|
||||
attachments: [],
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 통계 카드 =====
|
||||
const statCards: StatCard[] = useMemo(() => [
|
||||
{ label: '전체', value: `${stats.all}건`, icon: Files, iconColor: 'text-blue-500' },
|
||||
@@ -267,7 +354,7 @@ export function ReferenceBox() {
|
||||
], [stats]);
|
||||
|
||||
// ===== 테이블 컬럼 =====
|
||||
// 문서번호, 문서유형, 제목, 기안자, 기안일시, 상태, 확인
|
||||
// 문서번호, 문서유형, 제목, 기안자, 기안일시, 상태
|
||||
const tableColumns: TableColumn[] = useMemo(() => [
|
||||
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
|
||||
{ key: 'documentNo', label: '문서번호' },
|
||||
@@ -276,7 +363,6 @@ export function ReferenceBox() {
|
||||
{ key: 'drafter', label: '기안자' },
|
||||
{ key: 'draftDate', label: '기안일시' },
|
||||
{ key: 'status', label: '상태', className: 'text-center' },
|
||||
{ key: 'confirm', label: '확인', className: 'w-[80px] text-center' },
|
||||
], []);
|
||||
|
||||
// ===== 테이블 행 렌더링 =====
|
||||
@@ -284,8 +370,12 @@ export function ReferenceBox() {
|
||||
const isSelected = selectedItems.has(item.id);
|
||||
|
||||
return (
|
||||
<TableRow key={item.id} className="hover:bg-muted/50">
|
||||
<TableCell className="text-center">
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
onClick={() => handleDocumentClick(item)}
|
||||
>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={isSelected} onCheckedChange={() => toggleSelection(item.id)} />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{globalIndex}</TableCell>
|
||||
@@ -297,18 +387,13 @@ export function ReferenceBox() {
|
||||
<TableCell>{item.drafter}</TableCell>
|
||||
<TableCell>{item.draftDate}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge className={DOCUMENT_STATUS_COLORS[item.documentStatus]}>
|
||||
{DOCUMENT_STATUS_LABELS[item.documentStatus]}
|
||||
<Badge className={READ_STATUS_COLORS[item.readStatus]}>
|
||||
{READ_STATUS_LABELS[item.readStatus]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{item.readStatus === 'read' && (
|
||||
<Check className="h-4 w-4 mx-auto text-green-600" />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}, [selectedItems, toggleSelection]);
|
||||
}, [selectedItems, toggleSelection, handleDocumentClick]);
|
||||
|
||||
// ===== 모바일 카드 렌더링 =====
|
||||
const renderMobileCard = useCallback((
|
||||
@@ -325,14 +410,9 @@ export function ReferenceBox() {
|
||||
headerBadges={
|
||||
<div className="flex gap-1">
|
||||
<Badge variant="outline">{APPROVAL_TYPE_LABELS[item.approvalType]}</Badge>
|
||||
<Badge className={DOCUMENT_STATUS_COLORS[item.documentStatus]}>
|
||||
{DOCUMENT_STATUS_LABELS[item.documentStatus]}
|
||||
<Badge className={READ_STATUS_COLORS[item.readStatus]}>
|
||||
{READ_STATUS_LABELS[item.readStatus]}
|
||||
</Badge>
|
||||
{item.readStatus === 'read' ? (
|
||||
<Badge className="bg-gray-100 text-gray-800">열람</Badge>
|
||||
) : (
|
||||
<Badge className="bg-blue-100 text-blue-800">미열람</Badge>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
isSelected={isSelected}
|
||||
@@ -505,6 +585,17 @@ export function ReferenceBox() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 문서 상세 모달 */}
|
||||
{selectedDocument && (
|
||||
<DocumentDetailModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
documentType={getDocumentType(selectedDocument.approvalType)}
|
||||
data={convertToModalData(selectedDocument)}
|
||||
mode="reference"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -75,21 +75,6 @@ interface PaginatedResponse<T> {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답에서 프론트엔드 형식으로 변환
|
||||
*/
|
||||
@@ -117,12 +102,10 @@ function transformApiToFrontend(apiData: Record<string, unknown>): AttendanceRec
|
||||
*/
|
||||
export async function checkIn(
|
||||
data: CheckInRequest
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${process.env.API_URL}/v1/attendances/check-in`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/attendances/check-in`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
check_in: data.checkIn,
|
||||
@@ -136,6 +119,14 @@ export async function checkIn(
|
||||
}),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '출근 기록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -164,12 +155,10 @@ export async function checkIn(
|
||||
*/
|
||||
export async function checkOut(
|
||||
data: CheckOutRequest
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${process.env.API_URL}/v1/attendances/check-out`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/attendances/check-out`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
check_out: data.checkOut,
|
||||
@@ -183,6 +172,14 @@ export async function checkOut(
|
||||
}),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '퇴근 기록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -213,19 +210,26 @@ export async function getTodayAttendance(): Promise<{
|
||||
success: boolean;
|
||||
data?: AttendanceRecord;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.API_URL}/v1/attendances?date=${today}&per_page=1`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '근태 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Board, BoardApiData, BoardFormData } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -22,21 +22,6 @@ interface ApiResponse<T> {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 데이터 → 프론트엔드 타입 변환
|
||||
*/
|
||||
@@ -89,10 +74,8 @@ function transformFrontendToApi(data: BoardFormData & { boardCode?: string; desc
|
||||
export async function getBoards(filters?: {
|
||||
board_type?: string;
|
||||
search?: string;
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string }> {
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.board_type) params.append('board_type', filters.board_type);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
@@ -101,14 +84,16 @@ export async function getBoards(filters?: {
|
||||
// 테넌트 게시판만 조회 (시스템 게시판은 mng에서 관리)
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET boards error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -120,7 +105,7 @@ export async function getBoards(filters?: {
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -143,10 +128,8 @@ export async function getBoards(filters?: {
|
||||
export async function getTenantBoards(filters?: {
|
||||
board_type?: string;
|
||||
search?: string;
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string }> {
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.board_type) params.append('board_type', filters.board_type);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
@@ -154,20 +137,28 @@ export async function getTenantBoards(filters?: {
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET tenant boards error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '테넌트 게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<BoardApiData[]> = await response.json();
|
||||
let result: ApiResponse<BoardApiData[]>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '테넌트 게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -182,27 +173,32 @@ export async function getTenantBoards(filters?: {
|
||||
/**
|
||||
* 게시판 상세 조회 (코드 기반)
|
||||
*/
|
||||
export async function getBoardByCode(code: string): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
export async function getBoardByCode(code: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${code}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${code}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET board error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<BoardApiData> = await response.json();
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
@@ -216,27 +212,32 @@ export async function getBoardByCode(code: string): Promise<{ success: boolean;
|
||||
/**
|
||||
* 게시판 상세 조회 (ID 기반)
|
||||
*/
|
||||
export async function getBoardById(id: string): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
export async function getBoardById(id: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET board by id error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<BoardApiData> = await response.json();
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
@@ -252,21 +253,31 @@ export async function getBoardById(id: string): Promise<{ success: boolean; data
|
||||
*/
|
||||
export async function createBoard(
|
||||
data: BoardFormData & { boardCode: string; description?: string }
|
||||
): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -291,21 +302,31 @@ export async function createBoard(
|
||||
export async function updateBoard(
|
||||
id: string,
|
||||
data: BoardFormData & { boardCode?: string; description?: string }
|
||||
): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data, true); // isUpdate=true
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -327,19 +348,29 @@ export async function updateBoard(
|
||||
/**
|
||||
* 게시판 삭제
|
||||
*/
|
||||
export async function deleteBoard(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteBoard(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
let result: { success: boolean; message?: string };
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -358,10 +389,15 @@ export async function deleteBoard(id: string): Promise<{ success: boolean; error
|
||||
/**
|
||||
* 게시판 일괄 삭제
|
||||
*/
|
||||
export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const results = await Promise.all(ids.map(id => deleteBoard(id)));
|
||||
const failed = results.filter(r => !r.success);
|
||||
const hasAuthError = results.some(r => r.__authError);
|
||||
|
||||
if (hasAuthError) {
|
||||
return { success: false, error: '인증이 만료되었습니다.', __authError: true };
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
return {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
@@ -21,21 +21,6 @@ import type {
|
||||
Post,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 데이터 → 프론트엔드 타입 변환
|
||||
*/
|
||||
@@ -74,10 +59,8 @@ function transformApiToPost(apiData: PostApiData, boardName?: string): Post {
|
||||
export async function getPosts(
|
||||
boardCode: string,
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
@@ -88,14 +71,16 @@ export async function getPosts(
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET posts error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -107,7 +92,7 @@ export async function getPosts(
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -126,10 +111,8 @@ export async function getPosts(
|
||||
*/
|
||||
export async function getMyPosts(
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.board_code) params.append('board_code', filters.board_code);
|
||||
@@ -140,14 +123,16 @@ export async function getMyPosts(
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/my-posts${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET my-posts error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '나의 게시글 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -159,7 +144,7 @@ export async function getMyPosts(
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '나의 게시글 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -179,26 +164,26 @@ export async function getMyPosts(
|
||||
export async function getPost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: Post; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET post error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PostApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
@@ -221,25 +206,27 @@ export async function createPost(
|
||||
is_secret?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: Post; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '게시글 등록에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToPost(result.data) };
|
||||
@@ -262,25 +249,27 @@ export async function updatePost(
|
||||
is_secret?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: Post; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '게시글 수정에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToPost(result.data) };
|
||||
@@ -296,24 +285,26 @@ export async function updatePost(
|
||||
export async function deletePost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '게시글 삭제에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
@@ -321,4 +312,4 @@ export async function deletePost(
|
||||
console.error('[BoardActions] deletePost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
src/components/business/juil/JuilDashboard.tsx
Normal file
19
src/components/business/juil/JuilDashboard.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from "react";
|
||||
import { JuilMainDashboard } from "./JuilMainDashboard";
|
||||
import { PageLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
|
||||
/**
|
||||
* JuilDashboard - 주일기업 전용 대시보드
|
||||
*
|
||||
* 건설/공사 프로젝트 중심의 메트릭과 현황을 보여줍니다.
|
||||
*/
|
||||
export function JuilDashboard() {
|
||||
console.log('🏗️ Juil Dashboard rendering...');
|
||||
return (
|
||||
<Suspense fallback={<PageLoadingSpinner text="공사 현황을 불러오는 중..." />}>
|
||||
<JuilMainDashboard />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
195
src/components/business/juil/JuilMainDashboard.tsx
Normal file
195
src/components/business/juil/JuilMainDashboard.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useCurrentTime } from "@/hooks/useCurrentTime";
|
||||
import {
|
||||
Building2,
|
||||
MapPin,
|
||||
HardHat,
|
||||
Truck,
|
||||
AlertTriangle,
|
||||
FileText,
|
||||
Calendar as CalendarIcon,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
ArrowUpRight,
|
||||
ClipboardList,
|
||||
Hammer
|
||||
} from "lucide-react";
|
||||
|
||||
export function JuilMainDashboard() {
|
||||
const currentTime = useCurrentTime();
|
||||
|
||||
// 가상 데이터: 건설 프로젝트 현황
|
||||
const projectStats = {
|
||||
totalProjects: 12,
|
||||
active: 5, // 진행중
|
||||
bidding: 3, // 입찰중
|
||||
planning: 4, // 설계/계획
|
||||
};
|
||||
|
||||
const recentProjects = [
|
||||
{ id: 1, name: "강남 데이터센터 신축공사", status: "공사중", progress: 45, manager: "김현장", deadline: "2024-12-30" },
|
||||
{ id: 2, name: "판교 오피스 리모델링", status: "마감", progress: 95, manager: "이공사", deadline: "2024-06-15" },
|
||||
{ id: 3, name: "부산 물류센터 증축", status: "착공준비", progress: 5, manager: "박안전", deadline: "2025-03-20" },
|
||||
];
|
||||
|
||||
const upcomingEvents = [
|
||||
{ id: 1, type: "현장설명회", title: "송도 주상복합 입찰 설명회", date: "2024-06-25 14:00", location: "인천 송도 현장" },
|
||||
{ id: 2, type: "입찰마감", title: "세종 스마트시티 관로공사", date: "2024-06-28 17:00", location: "전자조달" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6 space-y-6">
|
||||
{/* 헤더 섹션 */}
|
||||
<div className="bg-card border border-border/20 rounded-xl p-4 md:p-6 mb-4">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
|
||||
<Building2 className="w-8 h-8 text-blue-600" />
|
||||
주일기업 프로젝트 현황
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{currentTime} · 실시간 현장 모니터링
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<ClipboardList className="w-4 h-4 mr-2" />
|
||||
일일 작업보고
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상단 통계 카드 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">진행중인 현장</CardTitle>
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{projectStats.active}개소</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
총 {projectStats.totalProjects}개 프로젝트 중
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">금일 출력 인원</CardTitle>
|
||||
<HardHat className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">142명</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<span className="text-green-500 font-medium">+12명</span> (전일 대비)
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">입찰 진행</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{projectStats.bidding}건</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
마감 임박 1건
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">안전 이슈</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-600">0건</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
무재해 125일째
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 주요 프로젝트 진행 현황 */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Hammer className="w-5 h-5" />
|
||||
주요 프로젝트 진행 현황
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recentProjects.map((project) => (
|
||||
<div key={project.id} className="flex items-center justify-between p-4 bg-muted/40 rounded-lg border">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-bold">{project.name}</span>
|
||||
<Badge variant={project.status === '공사중' ? 'default' : 'secondary'}>
|
||||
{project.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<HardHat className="w-3 h-3" /> {project.manager}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CalendarIcon className="w-3 h-3" /> {project.deadline}까지
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-32 text-right">
|
||||
<div className="text-2xl font-bold text-blue-600">{project.progress}%</div>
|
||||
<div className="text-xs text-muted-foreground">공정률</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 일정 / 알림 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
주요 일정 (현설/입찰)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{upcomingEvents.map((event) => (
|
||||
<div key={event.id} className="flex gap-3 items-start border-l-4 border-blue-200 pl-3 py-1">
|
||||
<div className="flex-1">
|
||||
<Badge variant="outline" className="mb-1 text-xs">
|
||||
{event.type}
|
||||
</Badge>
|
||||
<p className="font-medium text-sm">{event.title}</p>
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
|
||||
<span>{event.date}</span>
|
||||
<span>|</span>
|
||||
<span>{event.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="ghost" className="w-full text-sm text-muted-foreground">
|
||||
전체 일정 보기
|
||||
<ArrowUpRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
493
src/components/business/juil/partners/PartnerListClient.tsx
Normal file
493
src/components/business/juil/partners/PartnerListClient.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Building2, Plus, Pencil, Trash2, AlertTriangle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { IntegratedListTemplateV2, TabOption, TableColumn } from '@/components/templates/IntegratedListTemplateV2';
|
||||
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { Partner, PartnerStats } from './types';
|
||||
import { getPartnerList, deletePartner, deletePartners, getPartnerStats } from './actions';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns: TableColumn[] = [
|
||||
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
|
||||
{ key: 'partnerCode', label: '거래처번호', className: 'w-[100px]' },
|
||||
{ key: 'category', label: '구분', className: 'w-[80px] text-center' },
|
||||
{ key: 'partnerName', label: '거래처명', className: 'min-w-[150px]' },
|
||||
{ key: 'representative', label: '대표자', className: 'w-[100px]' },
|
||||
{ key: 'manager', label: '담당자', className: 'w-[100px]' },
|
||||
{ key: 'phone', label: '전화번호', className: 'w-[130px]' },
|
||||
{ key: 'paymentDay', label: '매출 결제일', className: 'w-[100px] text-center' },
|
||||
{ key: 'isBadDebt', label: '악성채권', className: 'w-[90px] text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'w-[100px] text-center' },
|
||||
];
|
||||
|
||||
interface PartnerListClientProps {
|
||||
initialData?: Partner[];
|
||||
initialStats?: PartnerStats;
|
||||
}
|
||||
|
||||
export default function PartnerListClient({ initialData = [], initialStats }: PartnerListClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// 상태
|
||||
const [partners, setPartners] = useState<Partner[]>(initialData);
|
||||
const [stats, setStats] = useState<PartnerStats>(
|
||||
initialStats ?? { total: 0, unregistered: 0, badDebt: 0, normal: 0 }
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [badDebtFilter, setBadDebtFilter] = useState<'all' | 'badDebt' | 'normal'>('all');
|
||||
const [sortBy, setSortBy] = useState<'latest' | 'oldest' | 'nameAsc' | 'nameDesc'>('latest');
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false);
|
||||
const itemsPerPage = 20;
|
||||
|
||||
// 데이터 로드
|
||||
const loadData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [listResult, statsResult] = await Promise.all([
|
||||
getPartnerList({ size: 1000, badDebtFilter, sortBy }),
|
||||
getPartnerStats(),
|
||||
]);
|
||||
|
||||
if (listResult.success && listResult.data) {
|
||||
setPartners(listResult.data.items);
|
||||
}
|
||||
if (statsResult.success && statsResult.data) {
|
||||
setStats(statsResult.data);
|
||||
}
|
||||
} catch {
|
||||
toast.error('데이터 로드에 실패했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [badDebtFilter, sortBy]);
|
||||
|
||||
// 초기 데이터가 없으면 로드
|
||||
useEffect(() => {
|
||||
if (initialData.length === 0) {
|
||||
loadData();
|
||||
}
|
||||
}, [initialData.length, loadData]);
|
||||
|
||||
// 필터링된 데이터
|
||||
const filteredPartners = useMemo(() => {
|
||||
return partners.filter((partner) => {
|
||||
// 탭 필터 (전체/신규)
|
||||
if (activeTab === 'new') {
|
||||
// 신규 조건 (예: 최근 7일 이내 등록)
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
if (new Date(partner.createdAt) < sevenDaysAgo) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 악성채권 필터
|
||||
if (badDebtFilter === 'badDebt' && !partner.isBadDebt) {
|
||||
return false;
|
||||
}
|
||||
if (badDebtFilter === 'normal' && partner.isBadDebt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 검색 필터
|
||||
if (searchValue) {
|
||||
const search = searchValue.toLowerCase();
|
||||
return (
|
||||
partner.partnerCode.toLowerCase().includes(search) ||
|
||||
partner.partnerName.toLowerCase().includes(search) ||
|
||||
partner.representative.toLowerCase().includes(search) ||
|
||||
partner.manager.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [partners, activeTab, badDebtFilter, searchValue]);
|
||||
|
||||
// 정렬
|
||||
const sortedPartners = useMemo(() => {
|
||||
const sorted = [...filteredPartners];
|
||||
switch (sortBy) {
|
||||
case 'latest':
|
||||
sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
break;
|
||||
case 'oldest':
|
||||
sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||
break;
|
||||
case 'nameAsc':
|
||||
sorted.sort((a, b) => a.partnerName.localeCompare(b.partnerName));
|
||||
break;
|
||||
case 'nameDesc':
|
||||
sorted.sort((a, b) => b.partnerName.localeCompare(a.partnerName));
|
||||
break;
|
||||
}
|
||||
return sorted;
|
||||
}, [filteredPartners, sortBy]);
|
||||
|
||||
// 페이지네이션
|
||||
const totalPages = Math.ceil(sortedPartners.length / itemsPerPage);
|
||||
const paginatedData = useMemo(() => {
|
||||
const start = (currentPage - 1) * itemsPerPage;
|
||||
return sortedPartners.slice(start, start + itemsPerPage);
|
||||
}, [sortedPartners, currentPage, itemsPerPage]);
|
||||
|
||||
// 탭 옵션
|
||||
const tabOptions: TabOption[] = useMemo(
|
||||
() => [
|
||||
{ value: 'all', label: '전체', count: stats.total },
|
||||
{ value: 'new', label: '신규', count: stats.unregistered },
|
||||
],
|
||||
[stats]
|
||||
);
|
||||
|
||||
// 핸들러
|
||||
const handleTabChange = useCallback((value: string) => {
|
||||
setActiveTab(value);
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
const handleToggleSelection = useCallback((id: string) => {
|
||||
setSelectedItems((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleSelectAll = useCallback(() => {
|
||||
if (selectedItems.size === paginatedData.length) {
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
setSelectedItems(new Set(paginatedData.map((p) => p.id)));
|
||||
}
|
||||
}, [selectedItems.size, paginatedData]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(partner: Partner) => {
|
||||
router.push(`/ko/juil/project/bidding/partners/${partner.id}`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/juil/project/bidding/partners/new');
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(e: React.MouseEvent, partnerId: string) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/ko/juil/project/bidding/partners/${partnerId}/edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback((e: React.MouseEvent, partnerId: string) => {
|
||||
e.stopPropagation();
|
||||
setDeleteTargetId(partnerId);
|
||||
setDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteConfirm = useCallback(async () => {
|
||||
if (!deleteTargetId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await deletePartner(deleteTargetId);
|
||||
if (result.success) {
|
||||
toast.success('거래처가 삭제되었습니다.');
|
||||
setPartners((prev) => prev.filter((p) => p.id !== deleteTargetId));
|
||||
setSelectedItems((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(deleteTargetId);
|
||||
return newSet;
|
||||
});
|
||||
// 통계 재조회
|
||||
const statsResult = await getPartnerStats();
|
||||
if (statsResult.success && statsResult.data) {
|
||||
setStats(statsResult.data);
|
||||
}
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('삭제 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setDeleteDialogOpen(false);
|
||||
setDeleteTargetId(null);
|
||||
}
|
||||
}, [deleteTargetId]);
|
||||
|
||||
const handleBulkDeleteClick = useCallback(() => {
|
||||
if (selectedItems.size === 0) {
|
||||
toast.warning('삭제할 항목을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
setBulkDeleteDialogOpen(true);
|
||||
}, [selectedItems.size]);
|
||||
|
||||
const handleBulkDeleteConfirm = useCallback(async () => {
|
||||
if (selectedItems.size === 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const ids = Array.from(selectedItems);
|
||||
const result = await deletePartners(ids);
|
||||
if (result.success) {
|
||||
toast.success(`${result.deletedCount}개 항목이 삭제되었습니다.`);
|
||||
await loadData();
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
toast.error(result.error || '일괄 삭제에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('일괄 삭제 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setBulkDeleteDialogOpen(false);
|
||||
}
|
||||
}, [selectedItems, loadData]);
|
||||
|
||||
// 테이블 행 렌더링
|
||||
const renderTableRow = useCallback(
|
||||
(partner: Partner, index: number, globalIndex: number) => {
|
||||
const isSelected = selectedItems.has(partner.id);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={partner.id}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleRowClick(partner)}
|
||||
>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => handleToggleSelection(partner.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
|
||||
<TableCell className="font-medium">{partner.partnerCode}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="secondary">{partner.category}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{partner.partnerName}</TableCell>
|
||||
<TableCell>{partner.representative}</TableCell>
|
||||
<TableCell>{partner.manager}</TableCell>
|
||||
<TableCell>{partner.phone}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{partner.paymentDay ? `${partner.paymentDay}일` : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{partner.isBadDebt ? (
|
||||
<Badge variant="destructive">악성채권</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={(e) => handleEdit(e, partner.id)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={(e) => handleDeleteClick(e, partner.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
[selectedItems, handleToggleSelection, handleRowClick, handleEdit, handleDeleteClick]
|
||||
);
|
||||
|
||||
// 모바일 카드 렌더링
|
||||
const renderMobileCard = useCallback(
|
||||
(partner: Partner, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void) => {
|
||||
return (
|
||||
<MobileCard
|
||||
title={partner.partnerName}
|
||||
subtitle={partner.partnerCode}
|
||||
badge={partner.isBadDebt ? '악성채권' : undefined}
|
||||
badgeVariant={partner.isBadDebt ? 'destructive' : 'secondary'}
|
||||
isSelected={isSelected}
|
||||
onToggle={onToggle}
|
||||
onClick={() => handleRowClick(partner)}
|
||||
details={[
|
||||
{ label: '구분', value: partner.category },
|
||||
{ label: '대표자', value: partner.representative },
|
||||
{ label: '담당자', value: partner.manager },
|
||||
{ label: '전화번호', value: partner.phone },
|
||||
{ label: '매출 결제일', value: partner.paymentDay ? `${partner.paymentDay}일` : '-' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[handleRowClick]
|
||||
);
|
||||
|
||||
// 헤더 액션 (필터 + 등록 버튼)
|
||||
const headerActions = (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 악성채권 필터 */}
|
||||
<Select value={badDebtFilter} onValueChange={(v) => setBadDebtFilter(v as typeof badDebtFilter)}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="badDebt">악성채권</SelectItem>
|
||||
<SelectItem value="normal">정상</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 정렬 */}
|
||||
<Select value={sortBy} onValueChange={(v) => setSortBy(v as typeof sortBy)}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue placeholder="최신순" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="latest">최신순</SelectItem>
|
||||
<SelectItem value="oldest">등록순</SelectItem>
|
||||
<SelectItem value="nameAsc">거래처명 오름차순</SelectItem>
|
||||
<SelectItem value="nameDesc">거래처명 내림차순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 등록 버튼 */}
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
거래처 등록
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedListTemplateV2
|
||||
title="거래처 관리"
|
||||
description="거래처 정보를 관리합니다"
|
||||
icon={Building2}
|
||||
headerActions={headerActions}
|
||||
stats={[
|
||||
{
|
||||
label: '전체 거래처',
|
||||
value: stats.total,
|
||||
icon: Building2,
|
||||
iconColor: 'text-blue-500',
|
||||
},
|
||||
{
|
||||
label: '미등록',
|
||||
value: stats.unregistered,
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-orange-500',
|
||||
},
|
||||
]}
|
||||
tabs={tabOptions}
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchPlaceholder="거래처명, 거래처번호, 대표자 검색"
|
||||
tableColumns={tableColumns}
|
||||
data={paginatedData}
|
||||
allData={sortedPartners}
|
||||
getItemId={(item) => item.id}
|
||||
renderTableRow={renderTableRow}
|
||||
renderMobileCard={renderMobileCard}
|
||||
selectedItems={selectedItems}
|
||||
onToggleSelection={handleToggleSelection}
|
||||
onToggleSelectAll={handleToggleSelectAll}
|
||||
onBulkDelete={handleBulkDeleteClick}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems: sortedPartners.length,
|
||||
itemsPerPage,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 단일 삭제 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>거래처 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 거래처를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteConfirm}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 일괄 삭제 다이얼로그 */}
|
||||
<AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>거래처 일괄 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 {selectedItems.size}개 거래처를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleBulkDeleteConfirm}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
223
src/components/business/juil/partners/actions.ts
Normal file
223
src/components/business/juil/partners/actions.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
'use server';
|
||||
|
||||
import type { Partner, PartnerStats, PartnerFilter, PartnerListResponse } from './types';
|
||||
|
||||
/**
|
||||
* 주일 기업 - 거래처 관리 Server Actions
|
||||
* TODO: 실제 API 연동 시 구현
|
||||
*/
|
||||
|
||||
// 목업 데이터
|
||||
const mockPartners: Partner[] = [
|
||||
{
|
||||
id: '1',
|
||||
partnerCode: '123123',
|
||||
category: '매출',
|
||||
partnerName: '회사명',
|
||||
representative: '이름',
|
||||
manager: '이름',
|
||||
phone: '010-1234-1234',
|
||||
paymentDay: 15,
|
||||
isBadDebt: false,
|
||||
isActive: true,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
partnerCode: '123123',
|
||||
category: '매출',
|
||||
partnerName: '회사명',
|
||||
representative: '이름',
|
||||
manager: '이름',
|
||||
phone: '02-1234-1234',
|
||||
paymentDay: 15,
|
||||
isBadDebt: false,
|
||||
isActive: true,
|
||||
createdAt: '2025-01-02',
|
||||
updatedAt: '2025-01-02',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
partnerCode: '123123',
|
||||
category: '매출',
|
||||
partnerName: '회사명',
|
||||
representative: '이름',
|
||||
manager: '이름',
|
||||
phone: '010-1234-1234',
|
||||
paymentDay: 15,
|
||||
isBadDebt: true,
|
||||
isActive: true,
|
||||
createdAt: '2025-01-03',
|
||||
updatedAt: '2025-01-03',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
partnerCode: '123123',
|
||||
category: '매출',
|
||||
partnerName: '회사명',
|
||||
representative: '이름',
|
||||
manager: '이름',
|
||||
phone: '02-1234-1234',
|
||||
paymentDay: 15,
|
||||
isBadDebt: false,
|
||||
isActive: true,
|
||||
createdAt: '2025-01-04',
|
||||
updatedAt: '2025-01-04',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
partnerCode: '123123',
|
||||
category: '매출',
|
||||
partnerName: '회사명',
|
||||
representative: '이름',
|
||||
manager: '이름',
|
||||
phone: '010-1234-1234',
|
||||
paymentDay: 15,
|
||||
isBadDebt: false,
|
||||
isActive: true,
|
||||
createdAt: '2025-01-05',
|
||||
updatedAt: '2025-01-05',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
partnerCode: '123123',
|
||||
category: '매출',
|
||||
partnerName: '회사명',
|
||||
representative: '이름',
|
||||
manager: '이름',
|
||||
phone: '010-1234-1234',
|
||||
paymentDay: 15,
|
||||
isBadDebt: false,
|
||||
isActive: true,
|
||||
createdAt: '2025-01-06',
|
||||
updatedAt: '2025-01-06',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
partnerCode: '123123',
|
||||
category: '매출',
|
||||
partnerName: '회사명',
|
||||
representative: '이름',
|
||||
manager: '이름',
|
||||
phone: '010-1234-1234',
|
||||
paymentDay: 15,
|
||||
isBadDebt: false,
|
||||
isActive: true,
|
||||
createdAt: '2025-01-07',
|
||||
updatedAt: '2025-01-07',
|
||||
},
|
||||
];
|
||||
|
||||
// 거래처 목록 조회
|
||||
export async function getPartnerList(
|
||||
filter?: PartnerFilter
|
||||
): Promise<{ success: boolean; data?: PartnerListResponse; error?: string }> {
|
||||
try {
|
||||
// TODO: 실제 API 호출
|
||||
// const response = await fetch(`${API_URL}/partners`, { ... });
|
||||
|
||||
let filtered = [...mockPartners];
|
||||
|
||||
// 검색 필터
|
||||
if (filter?.search) {
|
||||
const search = filter.search.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(p) =>
|
||||
p.partnerName.toLowerCase().includes(search) ||
|
||||
p.partnerCode.toLowerCase().includes(search) ||
|
||||
p.representative.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
// 악성채권 필터
|
||||
if (filter?.badDebtFilter && filter.badDebtFilter !== 'all') {
|
||||
filtered = filtered.filter((p) =>
|
||||
filter.badDebtFilter === 'badDebt' ? p.isBadDebt : !p.isBadDebt
|
||||
);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
if (filter?.sortBy) {
|
||||
switch (filter.sortBy) {
|
||||
case 'latest':
|
||||
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
break;
|
||||
case 'oldest':
|
||||
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||
break;
|
||||
case 'nameAsc':
|
||||
filtered.sort((a, b) => a.partnerName.localeCompare(b.partnerName));
|
||||
break;
|
||||
case 'nameDesc':
|
||||
filtered.sort((a, b) => b.partnerName.localeCompare(a.partnerName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const page = filter?.page ?? 1;
|
||||
const size = filter?.size ?? 20;
|
||||
const start = (page - 1) * size;
|
||||
const paginatedItems = filtered.slice(start, start + size);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: paginatedItems,
|
||||
total: filtered.length,
|
||||
page,
|
||||
size,
|
||||
totalPages: Math.ceil(filtered.length / size),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('getPartnerList error:', error);
|
||||
return { success: false, error: '거래처 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 거래처 통계 조회
|
||||
export async function getPartnerStats(): Promise<{ success: boolean; data?: PartnerStats; error?: string }> {
|
||||
try {
|
||||
// TODO: 실제 API 호출
|
||||
const total = mockPartners.length;
|
||||
const badDebt = mockPartners.filter((p) => p.isBadDebt).length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
total,
|
||||
unregistered: 5, // 목업
|
||||
badDebt,
|
||||
normal: total - badDebt,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('getPartnerStats error:', error);
|
||||
return { success: false, error: '통계 조회에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 거래처 삭제
|
||||
export async function deletePartner(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// TODO: 실제 API 호출
|
||||
console.log('Delete partner:', id);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('deletePartner error:', error);
|
||||
return { success: false, error: '거래처 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 거래처 일괄 삭제
|
||||
export async function deletePartners(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
|
||||
try {
|
||||
// TODO: 실제 API 호출
|
||||
console.log('Delete partners:', ids);
|
||||
return { success: true, deletedCount: ids.length };
|
||||
} catch (error) {
|
||||
console.error('deletePartners error:', error);
|
||||
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
3
src/components/business/juil/partners/index.ts
Normal file
3
src/components/business/juil/partners/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as PartnerListClient } from './PartnerListClient';
|
||||
export * from './types';
|
||||
export * from './actions';
|
||||
57
src/components/business/juil/partners/types.ts
Normal file
57
src/components/business/juil/partners/types.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 주일 기업 - 거래처 관리 타입 정의
|
||||
*/
|
||||
|
||||
// 거래처 타입
|
||||
export interface Partner {
|
||||
id: string;
|
||||
partnerCode: string; // 거래처번호
|
||||
category: string; // 구분 (건설사, 시공사 등)
|
||||
partnerName: string; // 거래처명
|
||||
representative: string; // 대표자
|
||||
manager: string; // 담당자
|
||||
phone: string; // 전화번호
|
||||
paymentDay: number | null; // 매출 결제일
|
||||
isBadDebt: boolean; // 악성채권 여부
|
||||
isActive: boolean; // 활성 상태
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 거래처 통계
|
||||
export interface PartnerStats {
|
||||
total: number; // 전체 거래처
|
||||
unregistered: number; // 미등록
|
||||
badDebt: number; // 악성채권
|
||||
normal: number; // 정상
|
||||
}
|
||||
|
||||
// 거래처 필터
|
||||
export interface PartnerFilter {
|
||||
search?: string;
|
||||
badDebtFilter?: 'all' | 'badDebt' | 'normal';
|
||||
sortBy?: 'latest' | 'oldest' | 'nameAsc' | 'nameDesc';
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
// 거래처 폼 데이터
|
||||
export interface PartnerFormData {
|
||||
partnerCode?: string;
|
||||
category: string;
|
||||
partnerName: string;
|
||||
representative: string;
|
||||
manager: string;
|
||||
phone: string;
|
||||
paymentDay: number | null;
|
||||
isBadDebt: boolean;
|
||||
}
|
||||
|
||||
// API 응답 타입
|
||||
export interface PartnerListResponse {
|
||||
items: Partner[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
@@ -16,31 +16,14 @@ import type {
|
||||
CommentsApiResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 목록 조회
|
||||
*/
|
||||
export async function getPosts(
|
||||
boardCode: SystemBoardCode,
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
@@ -51,14 +34,16 @@ export async function getPosts(
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[CustomerCenterActions] GET posts error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -70,7 +55,7 @@ export async function getPosts(
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -87,26 +72,26 @@ export async function getPosts(
|
||||
export async function getPost(
|
||||
boardCode: SystemBoardCode,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[CustomerCenterActions] GET post error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PostApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
@@ -128,25 +113,27 @@ export async function createPost(
|
||||
is_secret?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '게시글 등록에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
@@ -168,25 +155,27 @@ export async function updatePost(
|
||||
is_secret?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '게시글 수정에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
@@ -202,24 +191,26 @@ export async function updatePost(
|
||||
export async function deletePost(
|
||||
boardCode: SystemBoardCode,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '게시글 삭제에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
@@ -237,26 +228,26 @@ export async function deletePost(
|
||||
export async function getComments(
|
||||
boardCode: SystemBoardCode,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: CommentsApiResponse; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: CommentsApiResponse; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[CustomerCenterActions] GET comments error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<CommentsApiResponse> = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -274,25 +265,27 @@ export async function createComment(
|
||||
boardCode: SystemBoardCode,
|
||||
postId: number | string,
|
||||
content: string
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '댓글 등록에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '댓글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
@@ -310,25 +303,27 @@ export async function updateComment(
|
||||
postId: number | string,
|
||||
commentId: number | string,
|
||||
content: string
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '댓글 수정에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '댓글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
@@ -345,24 +340,26 @@ export async function deleteComment(
|
||||
boardCode: SystemBoardCode,
|
||||
postId: number | string,
|
||||
commentId: number | string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '댓글 삭제에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '댓글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
@@ -370,4 +367,4 @@ export async function deleteComment(
|
||||
console.error('[CustomerCenterActions] deleteComment error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
AttendanceRecord,
|
||||
AttendanceApiData,
|
||||
@@ -44,20 +44,8 @@ interface PaginatedResponse<T> {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답 데이터를 프론트엔드 형식으로 변환
|
||||
@@ -172,45 +160,35 @@ interface EmployeeApiData {
|
||||
* 사원 목록 조회 (근태 등록용)
|
||||
*/
|
||||
export async function getEmployeesForAttendance(): Promise<EmployeeOption[]> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('per_page', '100'); // API 최대 500, 드롭다운용 100 충분
|
||||
searchParams.set('status', 'active'); // 재직자만
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('per_page', '100'); // API 최대 500, 드롭다운용 100 충분
|
||||
searchParams.set('status', 'active'); // 재직자만
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees?${searchParams.toString()}`;
|
||||
const url = `${API_URL}/v1/employees?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[AttendanceActions] GET employees error:', response.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<EmployeeApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[AttendanceActions] No employees data');
|
||||
return [];
|
||||
}
|
||||
|
||||
// API는 TenantUserProfile을 반환하지만, Attendance.user_id는 User.id를 참조
|
||||
// 따라서 user.id를 사용해야 함
|
||||
return result.data.data.map((emp) => ({
|
||||
id: String(emp.user?.id || emp.user_id), // User.id 사용
|
||||
name: emp.user?.name || emp.name,
|
||||
department: emp.department?.name || emp.tenant_user_profile?.department?.name || '',
|
||||
position: emp.position_key || emp.tenant_user_profile?.position?.name || '',
|
||||
rank: emp.tenant_user_profile?.rank || '',
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] getEmployeesForAttendance error:', error);
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET employees error:', error?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<EmployeeApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[AttendanceActions] No employees data');
|
||||
return [];
|
||||
}
|
||||
|
||||
// API는 TenantUserProfile을 반환하지만, Attendance.user_id는 User.id를 참조
|
||||
// 따라서 user.id를 사용해야 함
|
||||
return result.data.data.map((emp) => ({
|
||||
id: String(emp.user?.id || emp.user_id), // User.id 사용
|
||||
name: emp.user?.name || emp.name,
|
||||
department: emp.department?.name || emp.tenant_user_profile?.department?.name || '',
|
||||
position: emp.position_key || emp.tenant_user_profile?.position?.name || '',
|
||||
rank: emp.tenant_user_profile?.rank || '',
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -232,86 +210,62 @@ export async function getAttendances(params?: {
|
||||
sort_by?: string;
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: AttendanceRecord[]; total: number; lastPage: number }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.user_id) searchParams.set('user_id', params.user_id);
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
if (params?.date_from) searchParams.set('date_from', params.date_from);
|
||||
if (params?.date_to) searchParams.set('date_to', params.date_to);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.user_id) searchParams.set('user_id', params.user_id);
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
if (params?.date_from) searchParams.set('date_from', params.date_from);
|
||||
if (params?.date_to) searchParams.set('date_to', params.date_to);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances?${searchParams.toString()}`;
|
||||
const url = `${API_URL}/v1/attendances?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[AttendanceActions] GET list error:', response.status);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<AttendanceApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[AttendanceActions] No data in response');
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
return {
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
total: result.data.total,
|
||||
lastPage: result.data.last_page,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] getAttendances error:', error);
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET list error:', error?.message);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<AttendanceApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[AttendanceActions] No data in response');
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
return {
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
total: result.data.total,
|
||||
lastPage: result.data.last_page,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 상세 조회
|
||||
*/
|
||||
export async function getAttendanceById(id: string): Promise<AttendanceRecord | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[AttendanceActions] GET attendance error:', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<AttendanceApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] getAttendanceById error:', error);
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET attendance error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<AttendanceApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,42 +274,32 @@ export async function getAttendanceById(id: string): Promise<AttendanceRecord |
|
||||
export async function createAttendance(
|
||||
data: AttendanceFormData
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[AttendanceActions] POST attendance request:', apiData);
|
||||
|
||||
console.log('[AttendanceActions] POST attendance request:', apiData);
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] POST attendance response:', result);
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] POST attendance response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] createAttendance error:', error);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: result.message || '근태 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -365,113 +309,81 @@ export async function updateAttendance(
|
||||
id: string,
|
||||
data: AttendanceFormData
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[AttendanceActions] PATCH attendance request:', apiData);
|
||||
|
||||
console.log('[AttendanceActions] PATCH attendance request:', apiData);
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/${id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] PATCH attendance response:', result);
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] PATCH attendance response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] updateAttendance error:', error);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: result.message || '근태 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 삭제
|
||||
*/
|
||||
export async function deleteAttendance(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, { method: 'DELETE' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] DELETE attendance response:', result);
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] DELETE attendance response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] deleteAttendance error:', error);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: result.message || '근태 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 일괄 삭제
|
||||
*/
|
||||
export async function deleteAttendances(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/bulk-delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/bulk-delete`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
}
|
||||
);
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] BULK DELETE attendance response:', result);
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] BULK DELETE attendance response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 일괄 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] deleteAttendances error:', error);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
error: result.message || '근태 일괄 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -482,43 +394,33 @@ export async function getMonthlyStats(params: {
|
||||
month: number;
|
||||
user_id?: string;
|
||||
}): Promise<AttendanceStats | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.set('year', String(params.year));
|
||||
searchParams.set('month', String(params.month));
|
||||
if (params.user_id) searchParams.set('user_id', params.user_id);
|
||||
searchParams.set('year', String(params.year));
|
||||
searchParams.set('month', String(params.month));
|
||||
if (params.user_id) searchParams.set('user_id', params.user_id);
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/monthly-stats?${searchParams.toString()}`;
|
||||
const url = `${API_URL}/v1/attendances/monthly-stats?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[AttendanceActions] GET monthly-stats error:', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
year: result.data.year,
|
||||
month: result.data.month,
|
||||
totalDays: result.data.total_days,
|
||||
byStatus: result.data.by_status,
|
||||
totalWorkMinutes: result.data.total_work_minutes,
|
||||
totalOvertimeMinutes: result.data.total_overtime_minutes,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[AttendanceActions] getMonthlyStats error:', error);
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET monthly-stats error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
year: result.data.year,
|
||||
month: result.data.month,
|
||||
totalDays: result.data.total_days,
|
||||
byStatus: result.data.by_status,
|
||||
totalWorkMinutes: result.data.total_work_minutes,
|
||||
totalOvertimeMinutes: result.data.total_overtime_minutes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Card, CardFormData, CardStatus } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -54,19 +54,6 @@ interface CardResponse {
|
||||
data: CardApiData;
|
||||
}
|
||||
|
||||
// API 헤더 생성
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// API URL (without double /api)
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL || process.env.API_URL}/api`;
|
||||
|
||||
@@ -147,153 +134,128 @@ export async function getCards(params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}): Promise<{ success: boolean; data?: Card[]; pagination?: { total: number; currentPage: number; lastPage: number }; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', mapFrontendStatusToApi(params.status as CardStatus));
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', mapFrontendStatusToApi(params.status as CardStatus));
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
|
||||
const url = `${API_URL}/v1/cards${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const url = `${API_URL}/v1/cards${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const result: CardListResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '카드 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
pagination: {
|
||||
total: result.data.total,
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getCards] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardListResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
pagination: {
|
||||
total: result.data.total,
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 상세 조회
|
||||
*/
|
||||
export async function getCard(id: string): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/cards/${id}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, { method: 'GET' });
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '카드 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getCard] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 등록
|
||||
*/
|
||||
export async function createCard(data: CardFormData): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_URL}/v1/cards`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '카드 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[createCard] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 수정
|
||||
*/
|
||||
export async function updateCard(id: string, data: CardFormData): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_URL}/v1/cards/${id}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '카드 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[updateCard] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 삭제
|
||||
*/
|
||||
export async function deleteCard(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/cards/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, { method: 'DELETE' });
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '카드 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[deleteCard] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,27 +281,22 @@ export async function deleteCards(ids: string[]): Promise<{ success: boolean; er
|
||||
* 카드 상태 토글
|
||||
*/
|
||||
export async function toggleCardStatus(id: string): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/cards/${id}/toggle`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}/toggle`, { method: 'PATCH' });
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[toggleCardStatus] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,36 +304,30 @@ export async function toggleCardStatus(id: string): Promise<{ success: boolean;
|
||||
* 주의: Card.assigned_user_id는 User.id를 참조하므로 user.id를 반환해야 함
|
||||
*/
|
||||
export async function getActiveEmployees(): Promise<{ success: boolean; data?: Array<{ id: string; label: string }>; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/employees?status=active&per_page=50`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/employees?status=active&per_page=50`, { method: 'GET' });
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '직원 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
// API는 TenantUserProfile을 반환하지만, Card.assigned_user_id는 User.id를 참조
|
||||
// 따라서 user.id를 사용해야 함
|
||||
const employees = result.data.data.map((emp: {
|
||||
id: number;
|
||||
user_id: number;
|
||||
user?: { id: number; name: string };
|
||||
department?: { name: string };
|
||||
position_key?: string;
|
||||
}) => ({
|
||||
id: String(emp.user?.id || emp.user_id), // User.id 사용
|
||||
label: `${emp.department?.name || ''} / ${emp.user?.name || ''} / ${emp.position_key || ''}`.replace(/^ \/ | \/ $/g, '').replace(/ \/ $/g, ''),
|
||||
}));
|
||||
|
||||
return { success: true, data: employees };
|
||||
} catch (error) {
|
||||
console.error('[getActiveEmployees] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '직원 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '직원 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
// API는 TenantUserProfile을 반환하지만, Card.assigned_user_id는 User.id를 참조
|
||||
// 따라서 user.id를 사용해야 함
|
||||
const employees = result.data.data.map((emp: {
|
||||
id: number;
|
||||
user_id: number;
|
||||
user?: { id: number; name: string };
|
||||
department?: { name: string };
|
||||
position_key?: string;
|
||||
}) => ({
|
||||
id: String(emp.user?.id || emp.user_id), // User.id 사용
|
||||
label: `${emp.department?.name || ''} / ${emp.user?.name || ''} / ${emp.position_key || ''}`.replace(/^ \/ | \/ $/g, '').replace(/ \/ $/g, ''),
|
||||
}));
|
||||
|
||||
return { success: true, data: employees };
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -91,20 +91,8 @@ interface ApiResponse<T> {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답을 프론트엔드 형식으로 변환 (재귀)
|
||||
@@ -141,43 +129,35 @@ function transformApiToFrontend(apiData: ApiDepartment, depth: number = 0): Depa
|
||||
export async function getDepartmentTree(params?: {
|
||||
withUsers?: boolean;
|
||||
}): Promise<{ success: boolean; data?: DepartmentRecord[]; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const queryParams = new URLSearchParams();
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params?.withUsers) {
|
||||
queryParams.append('with_users', '1');
|
||||
}
|
||||
if (params?.withUsers) {
|
||||
queryParams.append('with_users', '1');
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree${queryString ? `?${queryString}` : ''}`;
|
||||
const queryString = queryParams.toString();
|
||||
const url = `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const result: ApiResponse<ApiDepartment[]> = await response.json();
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 트리 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
if (result.success && result.data) {
|
||||
const transformed = result.data.map((dept) => transformApiToFrontend(dept, 0));
|
||||
return {
|
||||
success: true,
|
||||
data: transformed,
|
||||
};
|
||||
}
|
||||
const result: ApiResponse<ApiDepartment[]> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
const transformed = result.data.map((dept) => transformApiToFrontend(dept, 0));
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 트리 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getDepartmentTree] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '부서 트리 조회에 실패했습니다.',
|
||||
success: true,
|
||||
data: transformed,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 트리 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,33 +167,25 @@ export async function getDepartmentTree(params?: {
|
||||
export async function getDepartmentById(
|
||||
id: number
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/${id}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'GET' });
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getDepartmentById] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '부서 조회에 실패했습니다.',
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,41 +195,35 @@ export async function getDepartmentById(
|
||||
export async function createDepartment(
|
||||
data: CreateDepartmentRequest
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
parent_id: data.parentId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
parent_id: data.parentId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
});
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 생성에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[createDepartment] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '부서 생성에 실패했습니다.',
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 생성에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,41 +234,35 @@ export async function updateDepartment(
|
||||
id: number,
|
||||
data: UpdateDepartmentRequest
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
parent_id: data.parentId === null ? 0 : data.parentId, // null이면 0(최상위)으로 전환
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
parent_id: data.parentId === null ? 0 : data.parentId, // null이면 0(최상위)으로 전환
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
});
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 수정에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[updateDepartment] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '부서 수정에 실패했습니다.',
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,30 +272,22 @@ export async function updateDepartment(
|
||||
export async function deleteDepartment(
|
||||
id: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'DELETE' });
|
||||
|
||||
const result: ApiResponse<{ id: number; deleted_at: string }> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 삭제에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[deleteDepartment] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '부서 삭제에 실패했습니다.',
|
||||
};
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{ id: number; deleted_at: string }> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Users, Plus, Trash2, ArrowLeft, Save, Settings, Camera } from 'lucide-react';
|
||||
import { Users, Plus, Trash2, ArrowLeft, Save, Settings, Camera, Edit } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { FieldSettingsDialog } from './FieldSettingsDialog';
|
||||
import type {
|
||||
@@ -36,12 +37,23 @@ import {
|
||||
import { uploadProfileImage } from './actions';
|
||||
|
||||
interface EmployeeFormProps {
|
||||
mode: 'create' | 'edit';
|
||||
mode: 'create' | 'edit' | 'view';
|
||||
employee?: Employee;
|
||||
onSave: (data: EmployeeFormData) => void;
|
||||
onSave?: (data: EmployeeFormData) => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
fieldSettings?: FieldSettings;
|
||||
}
|
||||
|
||||
// 유효성 검사 에러 타입
|
||||
interface ValidationErrors {
|
||||
name?: string;
|
||||
email?: string;
|
||||
userId?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
const initialFormData: EmployeeFormData = {
|
||||
name: '',
|
||||
residentNumber: '',
|
||||
@@ -74,10 +86,16 @@ export function EmployeeForm({
|
||||
mode,
|
||||
employee,
|
||||
onSave,
|
||||
onEdit,
|
||||
onDelete,
|
||||
fieldSettings: initialFieldSettings = DEFAULT_FIELD_SETTINGS,
|
||||
}: EmployeeFormProps) {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const locale = params.locale as string || 'ko';
|
||||
const [formData, setFormData] = useState<EmployeeFormData>(initialFormData);
|
||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||
const isViewMode = mode === 'view';
|
||||
|
||||
// Daum 우편번호 서비스
|
||||
const { openPostcode } = useDaumPostcode({
|
||||
@@ -97,7 +115,12 @@ export function EmployeeForm({
|
||||
const [showFieldSettings, setShowFieldSettings] = useState(false);
|
||||
const [fieldSettings, setFieldSettings] = useState<FieldSettings>(initialFieldSettings);
|
||||
|
||||
const title = mode === 'create' ? '사원 등록' : '사원 수정';
|
||||
const title = mode === 'create' ? '사원 등록' : mode === 'edit' ? '사원 수정' : '사원 상세';
|
||||
const description = mode === 'create'
|
||||
? '새로운 사원 정보를 입력합니다'
|
||||
: mode === 'edit'
|
||||
? '사원 정보를 수정합니다'
|
||||
: '사원 정보를 확인합니다';
|
||||
|
||||
// localStorage에서 항목 설정 로드
|
||||
useEffect(() => {
|
||||
@@ -117,9 +140,9 @@ export function EmployeeForm({
|
||||
localStorage.setItem('employeeFieldSettings', JSON.stringify(newSettings));
|
||||
};
|
||||
|
||||
// 데이터 초기화
|
||||
// 데이터 초기화 (edit, view 모드)
|
||||
useEffect(() => {
|
||||
if (employee && mode === 'edit') {
|
||||
if (employee && (mode === 'edit' || mode === 'view')) {
|
||||
setFormData({
|
||||
name: employee.name,
|
||||
residentNumber: employee.residentNumber || '',
|
||||
@@ -153,6 +176,62 @@ export function EmployeeForm({
|
||||
// 입력 변경 핸들러
|
||||
const handleChange = (field: keyof EmployeeFormData, value: unknown) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 에러 초기화
|
||||
if (errors[field as keyof ValidationErrors]) {
|
||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
// 이메일 형식 검사
|
||||
const isValidEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
// 유효성 검사
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: ValidationErrors = {};
|
||||
|
||||
// 이름 필수
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = '이름을 입력해주세요.';
|
||||
}
|
||||
|
||||
// 이메일 필수 + 형식 검사
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = '이메일을 입력해주세요.';
|
||||
} else if (!isValidEmail(formData.email)) {
|
||||
newErrors.email = '올바른 이메일 형식이 아닙니다.';
|
||||
}
|
||||
|
||||
// 아이디 필수
|
||||
if (!formData.userId.trim()) {
|
||||
newErrors.userId = '아이디를 입력해주세요.';
|
||||
}
|
||||
|
||||
// 등록 모드일 때 비밀번호 검사
|
||||
if (mode === 'create') {
|
||||
if (!formData.password) {
|
||||
newErrors.password = '비밀번호를 입력해주세요.';
|
||||
} else if (formData.password.length < 6) {
|
||||
newErrors.password = '비밀번호는 6자 이상이어야 합니다.';
|
||||
}
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = '비밀번호가 일치하지 않습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
// 에러가 있으면 첫 번째 에러 메시지 표시
|
||||
const firstError = Object.values(newErrors)[0];
|
||||
if (firstError) {
|
||||
toast.error(firstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 부서/직책 추가
|
||||
@@ -191,12 +270,21 @@ export function EmployeeForm({
|
||||
// 저장
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave(formData);
|
||||
|
||||
// view 모드에서는 저장 불가
|
||||
if (isViewMode) return;
|
||||
|
||||
// 유효성 검사
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSave?.(formData);
|
||||
};
|
||||
|
||||
// 취소
|
||||
// 취소 (목록으로 이동)
|
||||
const handleCancel = () => {
|
||||
router.back();
|
||||
router.push(`/${locale}/hr/employee-management`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -205,17 +293,19 @@ export function EmployeeForm({
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<PageHeader
|
||||
title={title}
|
||||
description={mode === 'create' ? '새로운 사원 정보를 입력합니다' : '사원 정보를 수정합니다'}
|
||||
description={description}
|
||||
icon={Users}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowFieldSettings(true)}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
항목 설정
|
||||
</Button>
|
||||
{!isViewMode && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowFieldSettings(true)}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
항목 설정
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
@@ -234,8 +324,10 @@ export function EmployeeForm({
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="이름을 입력하세요"
|
||||
required
|
||||
disabled={isViewMode}
|
||||
className={errors.name ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -245,6 +337,7 @@ export function EmployeeForm({
|
||||
value={formData.residentNumber}
|
||||
onChange={(e) => handleChange('residentNumber', e.target.value)}
|
||||
placeholder="000000-0000000"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -255,18 +348,22 @@ export function EmployeeForm({
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleChange('phone', e.target.value)}
|
||||
placeholder="010-0000-0000"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">이메일</Label>
|
||||
<Label htmlFor="email">이메일 *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleChange('email', e.target.value)}
|
||||
placeholder="email@company.com"
|
||||
disabled={isViewMode}
|
||||
className={errors.email ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.email && <p className="text-sm text-red-500">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -277,6 +374,7 @@ export function EmployeeForm({
|
||||
value={formData.salary}
|
||||
onChange={(e) => handleChange('salary', e.target.value)}
|
||||
placeholder="연봉 (원)"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,16 +387,19 @@ export function EmployeeForm({
|
||||
value={formData.bankAccount.bankName}
|
||||
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, bankName: e.target.value })}
|
||||
placeholder="은행명"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<Input
|
||||
value={formData.bankAccount.accountNumber}
|
||||
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountNumber: e.target.value })}
|
||||
placeholder="계좌번호"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<Input
|
||||
value={formData.bankAccount.accountHolder}
|
||||
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountHolder: e.target.value })}
|
||||
placeholder="예금주"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -318,32 +419,46 @@ export function EmployeeForm({
|
||||
{fieldSettings.showProfileImage && (
|
||||
<div className="space-y-2 flex-shrink-0">
|
||||
<Label>프로필 사진</Label>
|
||||
<div className="w-32 h-32 border border-dashed border-gray-300 rounded-md flex flex-col items-center justify-center bg-gray-50 relative cursor-pointer hover:bg-gray-100">
|
||||
<span className="text-sm text-gray-400 mb-1">IMG</span>
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<Camera className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/gif"
|
||||
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// 미리보기 즉시 표시
|
||||
handleChange('profileImage', URL.createObjectURL(file));
|
||||
// 서버에 업로드
|
||||
const result = await uploadProfileImage(file);
|
||||
if (result.success && result.data?.url) {
|
||||
handleChange('profileImage', result.data.url);
|
||||
<div className={`w-32 h-32 border border-dashed border-gray-300 rounded-md flex flex-col items-center justify-center bg-gray-50 relative ${isViewMode ? '' : 'cursor-pointer hover:bg-gray-100'}`}>
|
||||
{formData.profileImage ? (
|
||||
<img
|
||||
src={formData.profileImage}
|
||||
alt="프로필"
|
||||
className="w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-sm text-gray-400 mb-1">IMG</span>
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<Camera className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isViewMode && (
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/gif"
|
||||
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// 미리보기 즉시 표시
|
||||
handleChange('profileImage', URL.createObjectURL(file));
|
||||
// 서버에 업로드
|
||||
const result = await uploadProfileImage(file);
|
||||
if (result.success && result.data?.url) {
|
||||
handleChange('profileImage', result.data.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
1 250 X 250px, 10MB 이하의<br />PNG, JPEG, GIF
|
||||
</p>
|
||||
{!isViewMode && (
|
||||
<p className="text-xs text-gray-500">
|
||||
1 250 X 250px, 10MB 이하의<br />PNG, JPEG, GIF
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -358,6 +473,7 @@ export function EmployeeForm({
|
||||
value={formData.employeeCode}
|
||||
onChange={(e) => handleChange('employeeCode', e.target.value)}
|
||||
placeholder="사원코드를 입력해주세요"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -369,13 +485,14 @@ export function EmployeeForm({
|
||||
value={formData.gender}
|
||||
onValueChange={(value) => handleChange('gender', value)}
|
||||
className="flex items-center gap-4 h-10"
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="male" id="gender-male" />
|
||||
<RadioGroupItem value="male" id="gender-male" disabled={isViewMode} />
|
||||
<Label htmlFor="gender-male" className="font-normal cursor-pointer">남성</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="female" id="gender-female" />
|
||||
<RadioGroupItem value="female" id="gender-female" disabled={isViewMode} />
|
||||
<Label htmlFor="gender-female" className="font-normal cursor-pointer">여성</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
@@ -388,21 +505,25 @@ export function EmployeeForm({
|
||||
<div className="space-y-2">
|
||||
<Label>주소</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" variant="default" size="sm" onClick={openPostcode} className="bg-blue-500 hover:bg-blue-600">
|
||||
우편번호 찾기
|
||||
</Button>
|
||||
{!isViewMode && (
|
||||
<Button type="button" variant="default" size="sm" onClick={openPostcode} className="bg-blue-500 hover:bg-blue-600">
|
||||
우편번호 찾기
|
||||
</Button>
|
||||
)}
|
||||
<Input
|
||||
value={formData.address.zipCode}
|
||||
onChange={(e) => handleChange('address', { ...formData.address, zipCode: e.target.value })}
|
||||
placeholder=""
|
||||
className="w-24"
|
||||
readOnly
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<Input
|
||||
value={formData.address.address2}
|
||||
onChange={(e) => handleChange('address', { ...formData.address, address2: e.target.value })}
|
||||
placeholder="상세주소를 입력해주세요"
|
||||
className="flex-1"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,6 +550,7 @@ export function EmployeeForm({
|
||||
type="date"
|
||||
value={formData.hireDate}
|
||||
onChange={(e) => handleChange('hireDate', e.target.value)}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -439,8 +561,9 @@ export function EmployeeForm({
|
||||
<Select
|
||||
value={formData.employmentType}
|
||||
onValueChange={(value) => handleChange('employmentType', value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="고용형태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -460,6 +583,7 @@ export function EmployeeForm({
|
||||
value={formData.rank}
|
||||
onChange={(e) => handleChange('rank', e.target.value)}
|
||||
placeholder="직급 입력"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -470,8 +594,9 @@ export function EmployeeForm({
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(value) => handleChange('status', value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -489,20 +614,22 @@ export function EmployeeForm({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>부서/직책</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddDepartmentPosition}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
{!isViewMode && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddDepartmentPosition}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formData.departmentPositions.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-4 text-center border rounded-md">
|
||||
부서/직책을 추가해주세요
|
||||
{isViewMode ? '등록된 부서/직책이 없습니다' : '부서/직책을 추가해주세요'}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -513,21 +640,25 @@ export function EmployeeForm({
|
||||
onChange={(e) => handleDepartmentPositionChange(dp.id, 'departmentName', e.target.value)}
|
||||
placeholder="부서명"
|
||||
className="flex-1"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<Input
|
||||
value={dp.positionName}
|
||||
onChange={(e) => handleDepartmentPositionChange(dp.id, 'positionName', e.target.value)}
|
||||
placeholder="직책"
|
||||
className="flex-1"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveDepartmentPosition(dp.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
{!isViewMode && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveDepartmentPosition(dp.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -543,8 +674,9 @@ export function EmployeeForm({
|
||||
<Select
|
||||
value={formData.clockInLocation}
|
||||
onValueChange={(value) => handleChange('clockInLocation', value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="출근 위치 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -563,8 +695,9 @@ export function EmployeeForm({
|
||||
<Select
|
||||
value={formData.clockOutLocation}
|
||||
onValueChange={(value) => handleChange('clockOutLocation', value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="퇴근 위치 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -589,6 +722,7 @@ export function EmployeeForm({
|
||||
type="date"
|
||||
value={formData.resignationDate}
|
||||
onChange={(e) => handleChange('resignationDate', e.target.value)}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -601,6 +735,7 @@ export function EmployeeForm({
|
||||
value={formData.resignationReason}
|
||||
onChange={(e) => handleChange('resignationReason', e.target.value)}
|
||||
placeholder="퇴직 사유를 입력하세요"
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -625,7 +760,10 @@ export function EmployeeForm({
|
||||
value={formData.userId}
|
||||
onChange={(e) => handleChange('userId', e.target.value)}
|
||||
placeholder="사용자 아이디"
|
||||
disabled={isViewMode}
|
||||
className={errors.userId ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.userId && <p className="text-sm text-red-500">{errors.userId}</p>}
|
||||
</div>
|
||||
|
||||
{mode === 'create' && (
|
||||
@@ -638,18 +776,22 @@ export function EmployeeForm({
|
||||
value={formData.password}
|
||||
onChange={(e) => handleChange('password', e.target.value)}
|
||||
placeholder="비밀번호"
|
||||
className={errors.password ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.password && <p className="text-sm text-red-500">{errors.password}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">비밀번호 확인</Label>
|
||||
<Label htmlFor="confirmPassword">비밀번호 확인 *</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleChange('confirmPassword', e.target.value)}
|
||||
placeholder="비밀번호 확인"
|
||||
className={errors.confirmPassword ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.confirmPassword && <p className="text-sm text-red-500">{errors.confirmPassword}</p>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -659,8 +801,9 @@ export function EmployeeForm({
|
||||
<Select
|
||||
value={formData.role}
|
||||
onValueChange={(value) => handleChange('role', value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="권한 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -676,8 +819,9 @@ export function EmployeeForm({
|
||||
<Select
|
||||
value={formData.accountStatus}
|
||||
onValueChange={(value) => handleChange('accountStatus', value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -695,12 +839,25 @@ export function EmployeeForm({
|
||||
<div className="flex items-center justify-between">
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{mode === 'create' ? '등록' : '저장'}
|
||||
목록
|
||||
</Button>
|
||||
{isViewMode ? (
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" onClick={onEdit}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" onClick={onDelete}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="submit">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{mode === 'create' ? '등록' : '저장'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
* - DELETE /api/v1/employees/{id} - 삭제
|
||||
* - POST /api/v1/employees/bulk-delete - 일괄 삭제
|
||||
* - GET /api/v1/employees/stats - 통계
|
||||
*
|
||||
* 🚨 401 에러 시 __authError: true 반환 → 클라이언트에서 로그인 페이지로 리다이렉트
|
||||
*/
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Employee, EmployeeFormData, EmployeeStats } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi, type EmployeeApiData } from './utils';
|
||||
|
||||
@@ -35,25 +38,6 @@ interface PaginatedResponse<T> {
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
@@ -70,9 +54,8 @@ export async function getEmployees(params?: {
|
||||
has_account?: boolean;
|
||||
sort_by?: string;
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: Employee[]; total: number; lastPage: number }> {
|
||||
}): Promise<{ data: Employee[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -90,14 +73,15 @@ export async function getEmployees(params?: {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[EmployeeActions] GET list error:', response.status);
|
||||
// 🚨 401 인증 에러 → 클라이언트에서 로그인 페이지로 리다이렉트
|
||||
if (error?.__authError) {
|
||||
return { data: [], total: 0, lastPage: 1, __authError: true };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.error('[EmployeeActions] GET list error:', response?.status);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
@@ -122,21 +106,18 @@ export async function getEmployees(params?: {
|
||||
/**
|
||||
* 직원 상세 조회
|
||||
*/
|
||||
export async function getEmployeeById(id: string): Promise<Employee | null> {
|
||||
export async function getEmployeeById(id: string): Promise<Employee | null | { __authError: true }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { __authError: true };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[EmployeeActions] GET employee error:', response.status);
|
||||
if (!response || !response.ok) {
|
||||
console.error('[EmployeeActions] GET employee error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -158,21 +139,26 @@ export async function getEmployeeById(id: string): Promise<Employee | null> {
|
||||
*/
|
||||
export async function createEmployee(
|
||||
data: EmployeeFormData
|
||||
): Promise<{ success: boolean; data?: Employee; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Employee; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees`;
|
||||
|
||||
console.log('[EmployeeActions] POST employee request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] POST employee response:', result);
|
||||
@@ -203,21 +189,26 @@ export async function createEmployee(
|
||||
export async function updateEmployee(
|
||||
id: string,
|
||||
data: EmployeeFormData
|
||||
): Promise<{ success: boolean; data?: Employee; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Employee; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
|
||||
|
||||
console.log('[EmployeeActions] PATCH employee request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] PATCH employee response:', result);
|
||||
@@ -245,17 +236,19 @@ export async function updateEmployee(
|
||||
/**
|
||||
* 직원 삭제 (퇴직 처리)
|
||||
*/
|
||||
export async function deleteEmployee(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteEmployee(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] DELETE employee response:', result);
|
||||
@@ -280,18 +273,22 @@ export async function deleteEmployee(id: string): Promise<{ success: boolean; er
|
||||
/**
|
||||
* 직원 일괄 삭제
|
||||
*/
|
||||
export async function deleteEmployees(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteEmployees(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/bulk-delete`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/bulk-delete`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
}
|
||||
);
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] BULK DELETE employee response:', result);
|
||||
@@ -316,21 +313,18 @@ export async function deleteEmployees(ids: string[]): Promise<{ success: boolean
|
||||
/**
|
||||
* 직원 통계 조회
|
||||
*/
|
||||
export async function getEmployeeStats(): Promise<EmployeeStats | null> {
|
||||
export async function getEmployeeStats(): Promise<EmployeeStats | null | { __authError: true }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/stats`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/stats`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { __authError: true };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[EmployeeActions] GET stats error:', response.status);
|
||||
if (!response || !response.ok) {
|
||||
console.error('[EmployeeActions] GET stats error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -359,11 +353,17 @@ export async function uploadProfileImage(file: File): Promise<{
|
||||
success: boolean;
|
||||
data?: { url: string; path: string };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
// 토큰 없으면 인증 에러
|
||||
if (!token) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('directory', 'employees/profiles');
|
||||
@@ -373,13 +373,18 @@ export async function uploadProfileImage(file: File): Promise<{
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (response.status === 401) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: `파일 업로드 실패: ${response.status}` };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { SalaryRecord, SalaryDetail, PaymentStatus } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -85,19 +85,6 @@ interface BulkUpdateResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// API 헤더 생성
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL || process.env.API_URL}/api`;
|
||||
|
||||
@@ -184,46 +171,40 @@ export async function getSalaries(params?: {
|
||||
pagination?: { total: number; currentPage: number; lastPage: number };
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
|
||||
const url = `${API_URL}/v1/salaries${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const url = `${API_URL}/v1/salaries${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const result: SalaryListResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '급여 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
pagination: {
|
||||
total: result.data.total,
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getSalaries] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '급여 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: SalaryListResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '급여 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
pagination: {
|
||||
total: result.data.total,
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,28 +215,22 @@ export async function getSalary(id: string): Promise<{
|
||||
data?: SalaryDetail;
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/salaries/${id}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/${id}`, { method: 'GET' });
|
||||
|
||||
const result: SalaryResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '급여 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getSalary] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '급여 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: SalaryResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '급여 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,28 +240,25 @@ export async function updateSalaryStatus(
|
||||
id: string,
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; data?: SalaryRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/salaries/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
|
||||
const result: SalaryResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[updateSalaryStatus] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: SalaryResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,31 +268,28 @@ export async function bulkUpdateSalaryStatus(
|
||||
ids: string[],
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/salaries/bulk-update-status`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
status
|
||||
}),
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/bulk-update-status`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
status
|
||||
}),
|
||||
});
|
||||
|
||||
const result: BulkUpdateResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '일괄 상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedCount: result.data.updated_count,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[bulkUpdateSalaryStatus] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '일괄 상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: BulkUpdateResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '일괄 상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedCount: result.data.updated_count,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,44 +315,38 @@ export async function getSalaryStatistics(params?: {
|
||||
};
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
|
||||
const url = `${API_URL}/v1/salaries/statistics${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const url = `${API_URL}/v1/salaries/statistics${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
const result: StatisticsResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '통계 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalNetPayment: result.data.total_net_payment,
|
||||
totalBaseSalary: result.data.total_base_salary,
|
||||
totalAllowance: result.data.total_allowance,
|
||||
totalOvertime: result.data.total_overtime,
|
||||
totalBonus: result.data.total_bonus,
|
||||
totalDeduction: result.data.total_deduction,
|
||||
count: result.data.count,
|
||||
scheduledCount: result.data.scheduled_count,
|
||||
completedCount: result.data.completed_count,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getSalaryStatistics] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '통계 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: StatisticsResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '통계 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalNetPayment: result.data.total_net_payment,
|
||||
totalBaseSalary: result.data.total_base_salary,
|
||||
totalAllowance: result.data.total_allowance,
|
||||
totalOvertime: result.data.total_overtime,
|
||||
totalBonus: result.data.total_bonus,
|
||||
totalDeduction: result.data.total_deduction,
|
||||
count: result.data.count,
|
||||
scheduledCount: result.data.scheduled_count,
|
||||
completedCount: result.data.completed_count,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -171,25 +171,6 @@ interface PaginatedResponse<T> {
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL || process.env.API_URL}/api`;
|
||||
|
||||
@@ -266,7 +247,6 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
@@ -286,10 +266,11 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/leaves${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
||||
|
||||
@@ -328,11 +309,11 @@ export async function getLeaveById(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/${id}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
@@ -364,10 +345,8 @@ export async function createLeave(
|
||||
data: CreateLeaveRequest
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
leave_type: data.leaveType,
|
||||
@@ -378,6 +357,10 @@ export async function createLeave(
|
||||
}),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 신청에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -409,13 +392,15 @@ export async function approveLeave(
|
||||
comment?: string
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/${id}/approve`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/approve`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ comment }),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 승인에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -447,13 +432,15 @@ export async function rejectLeave(
|
||||
reason: string
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/${id}/reject`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/reject`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ reason }),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 반려에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -485,13 +472,15 @@ export async function cancelLeave(
|
||||
reason?: string
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/${id}/cancel`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/cancel`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ reason }),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 취소에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -524,15 +513,15 @@ export async function getMyLeaveBalance(year?: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = year
|
||||
? `${API_URL}/v1/leaves/balance?year=${year}`
|
||||
: `${API_URL}/v1/leaves/balance`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '잔여 휴가 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
@@ -569,15 +558,15 @@ export async function getUserLeaveBalance(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = year
|
||||
? `${API_URL}/v1/leaves/balance/${userId}?year=${year}`
|
||||
: `${API_URL}/v1/leaves/balance/${userId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '잔여 휴가 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
@@ -609,10 +598,8 @@ export async function setLeaveBalance(
|
||||
data: SetLeaveBalanceRequest
|
||||
): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/balance`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/balance`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
year: data.year,
|
||||
@@ -620,6 +607,10 @@ export async function setLeaveBalance(
|
||||
}),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '잔여 휴가 설정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -648,12 +639,14 @@ export async function setLeaveBalance(
|
||||
*/
|
||||
export async function deleteLeave(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/${id}`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<null> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
@@ -748,7 +741,6 @@ export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
@@ -764,10 +756,11 @@ export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/leaves/balances${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 사용현황 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
||||
|
||||
@@ -890,7 +883,6 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
@@ -910,10 +902,11 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/leaves/grants${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 부여 이력 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
||||
|
||||
@@ -950,10 +943,8 @@ export async function createLeaveGrant(
|
||||
data: CreateLeaveGrantRequest
|
||||
): Promise<{ success: boolean; data?: LeaveGrantRecord; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/grants`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/grants`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
grant_type: data.grantType,
|
||||
@@ -963,6 +954,10 @@ export async function createLeaveGrant(
|
||||
}),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 부여에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
@@ -991,12 +986,14 @@ export async function createLeaveGrant(
|
||||
*/
|
||||
export async function deleteLeaveGrant(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(`${API_URL}/v1/leaves/grants/${id}`, {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/grants/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 부여 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<null> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
@@ -1069,14 +1066,13 @@ export async function getActiveEmployees(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${API_URL}/v1/employees?status=active&per_page=100`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '직원 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { Search, Plus, Edit, Trash2, Package, Loader2 } from 'lucide-react';
|
||||
import { TableLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { useItemList } from '@/hooks/useItemList';
|
||||
import { handleApiError } from '@/lib/api/error-handler';
|
||||
import {
|
||||
IntegratedListTemplateV2,
|
||||
type TabOption,
|
||||
@@ -183,10 +184,15 @@ export default function ItemListClient() {
|
||||
},
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러 시 자동 로그인 페이지 리다이렉트
|
||||
if (!response.ok) {
|
||||
await handleApiError(response);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[Delete] 응답:', { status: response.status, result });
|
||||
|
||||
if (response.ok && result.success) {
|
||||
if (result.success) {
|
||||
refresh();
|
||||
} else {
|
||||
throw new Error(result.message || '삭제에 실패했습니다.');
|
||||
@@ -239,6 +245,13 @@ export default function ItemListClient() {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러 시 자동 로그인 페이지 리다이렉트
|
||||
if (response.status === 401) {
|
||||
await handleApiError(response);
|
||||
return; // 리다이렉트 후 중단
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok && result.success) {
|
||||
successCount++;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
ReceivingItem,
|
||||
ReceivingDetail,
|
||||
@@ -146,19 +146,6 @@ function transformProcessDataToApi(
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
@@ -180,9 +167,9 @@ export async function getReceivings(params?: {
|
||||
data: ReceivingItem[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -197,27 +184,33 @@ export async function getReceivings(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('[ReceivingActions] GET receivings:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ReceivingActions] GET receivings error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '입고 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
@@ -262,46 +255,32 @@ export async function getReceivingStats(): Promise<{
|
||||
success: boolean;
|
||||
data?: ReceivingStats;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/stats`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ReceivingActions] GET stats error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '입고 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입고 통계 조회에 실패했습니다.',
|
||||
};
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '입고 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToStats(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToStats(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ReceivingActions] getReceivingStats error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,88 +289,65 @@ export async function getReceivingById(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: ReceivingDetail;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[ReceivingActions] GET receiving error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '입고 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입고 조회에 실패했습니다.',
|
||||
};
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '입고 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ReceivingActions] getReceivingById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 입고 등록 =====
|
||||
export async function createReceiving(
|
||||
data: Partial<ReceivingDetail>
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[ReceivingActions] POST receiving request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
{ method: 'POST', body: JSON.stringify(apiData) }
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ReceivingActions] POST receiving response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입고 등록에 실패했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
if (!response) {
|
||||
return { success: false, error: '입고 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '입고 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ReceivingActions] createReceiving error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,77 +355,64 @@ export async function createReceiving(
|
||||
export async function updateReceiving(
|
||||
id: string,
|
||||
data: Partial<ReceivingDetail>
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[ReceivingActions] PUT receiving request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
{ method: 'PUT', body: JSON.stringify(apiData) }
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ReceivingActions] PUT receiving response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입고 수정에 실패했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
if (!response) {
|
||||
return { success: false, error: '입고 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '입고 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ReceivingActions] updateReceiving error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 입고 삭제 =====
|
||||
export async function deleteReceiving(
|
||||
id: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '입고 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ReceivingActions] DELETE receiving response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입고 삭제에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '입고 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[ReceivingActions] deleteReceiving error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,41 +420,32 @@ export async function deleteReceiving(
|
||||
export async function processReceiving(
|
||||
id: string,
|
||||
data: ReceivingProcessFormData
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformProcessDataToApi(data);
|
||||
|
||||
console.log('[ReceivingActions] POST process request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}/process`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
{ method: 'POST', body: JSON.stringify(apiData) }
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ReceivingActions] POST process response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '입고처리에 실패했습니다.',
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
if (!response) {
|
||||
return { success: false, error: '입고처리에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '입고처리에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ReceivingActions] processReceiving error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
StockItem,
|
||||
StockDetail,
|
||||
@@ -156,18 +156,6 @@ function transformApiToStats(data: StockApiStatsResponse): StockStats {
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
@@ -192,9 +180,9 @@ export async function getStocks(params?: {
|
||||
data: StockItem[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -213,27 +201,33 @@ export async function getStocks(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
console.log('[StockActions] GET stocks:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[StockActions] GET stocks error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '재고 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
@@ -278,46 +272,32 @@ export async function getStockStats(): Promise<{
|
||||
success: boolean;
|
||||
data?: StockStats;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/stats`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[StockActions] GET stats error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '재고 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '재고 통계 조회에 실패했습니다.',
|
||||
};
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '재고 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToStats(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToStats(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[StockActions] getStockStats error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,46 +306,32 @@ export async function getStockStatsByType(): Promise<{
|
||||
success: boolean;
|
||||
data?: StockApiStatsByTypeResponse;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/stats-by-type`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[StockActions] GET stats-by-type error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '품목유형별 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '품목유형별 통계 조회에 실패했습니다.',
|
||||
};
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '품목유형별 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[StockActions] getStockStatsByType error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,45 +340,31 @@ export async function getStockById(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: StockDetail;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[StockActions] GET stock error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '재고 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '재고 조회에 실패했습니다.',
|
||||
};
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '재고 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[StockActions] getStockById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,8 @@ export function OrderDocumentModal({
|
||||
|
||||
{/* 버튼 영역 - 고정 */}
|
||||
<div className="flex-shrink-0 flex items-center gap-2 px-6 py-3 bg-muted/30 border-b">
|
||||
<Button variant="outline" size="sm" onClick={handleSharePdf}>
|
||||
{/* TODO: 공유 기능 추가 예정 - PDF 다운로드, 이메일, 팩스, 카카오톡 공유 */}
|
||||
{/* <Button variant="outline" size="sm" onClick={handleSharePdf}>
|
||||
<FileDown className="h-4 w-4 mr-1" />
|
||||
PDF
|
||||
</Button>
|
||||
@@ -178,7 +179,7 @@ export function OrderDocumentModal({
|
||||
<Button variant="outline" size="sm" onClick={handleShareKakao}>
|
||||
<Share2 className="h-4 w-4 mr-1" />
|
||||
공유
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button variant="outline" size="sm" onClick={handlePrint}>
|
||||
<Printer className="h-4 w-4 mr-1" />
|
||||
인쇄
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
ShipmentItem,
|
||||
ShipmentDetail,
|
||||
@@ -233,18 +233,6 @@ function transformEditFormToApi(
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
@@ -273,9 +261,9 @@ export async function getShipments(params?: {
|
||||
data: ShipmentItem[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -304,19 +292,28 @@ export async function getShipments(params?: {
|
||||
|
||||
console.log('[ShipmentActions] GET shipments:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ShipmentActions] GET shipments error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[ShipmentActions] GET shipments error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -367,46 +364,36 @@ export async function getShipmentStats(): Promise<{
|
||||
success: boolean;
|
||||
data?: ShipmentStats;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/stats`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ShipmentActions] GET stats error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[ShipmentActions] GET stats error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출하 통계 조회에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '출하 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToStats(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToStats(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] getShipmentStats error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,46 +402,36 @@ export async function getShipmentStatsByStatus(): Promise<{
|
||||
success: boolean;
|
||||
data?: ShipmentApiStatsByStatusResponse;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/stats-by-status`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ShipmentActions] GET stats-by-status error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[ShipmentActions] GET stats-by-status error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '상태별 통계 조회에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '상태별 통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] getShipmentStatsByStatus error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,88 +440,74 @@ export async function getShipmentById(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: ShipmentDetail;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[ShipmentActions] GET shipment error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.error('[ShipmentActions] GET shipment error:', response?.status);
|
||||
return { success: false, error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출하 조회에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '출하 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] getShipmentById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 출하 등록 =====
|
||||
export async function createShipment(
|
||||
data: ShipmentCreateFormData
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformCreateFormToApi(data);
|
||||
|
||||
console.log('[ShipmentActions] POST shipment request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '출하 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ShipmentActions] POST shipment response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출하 등록에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '출하 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] createShipment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,42 +515,38 @@ export async function createShipment(
|
||||
export async function updateShipment(
|
||||
id: string,
|
||||
data: Partial<ShipmentEditFormData>
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformEditFormToApi(data);
|
||||
|
||||
console.log('[ShipmentActions] PUT shipment request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '출하 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ShipmentActions] PUT shipment response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출하 수정에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '출하 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] updateShipment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,10 +562,8 @@ export async function updateShipmentStatus(
|
||||
driverContact?: string;
|
||||
confirmedArrival?: string;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const apiData: Record<string, unknown> = { status };
|
||||
if (additionalData?.loadingTime) apiData.loading_time = additionalData.loadingTime;
|
||||
if (additionalData?.loadingCompletedAt) apiData.loading_completed_at = additionalData.loadingCompletedAt;
|
||||
@@ -617,70 +574,67 @@ export async function updateShipmentStatus(
|
||||
|
||||
console.log('[ShipmentActions] PATCH status request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}/status`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ShipmentActions] PATCH status response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '상태 변경에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
return { success: true, data: transformApiToDetail(result.data) };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] updateShipmentStatus error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 출하 삭제 =====
|
||||
export async function deleteShipment(
|
||||
id: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '출하 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[ShipmentActions] DELETE shipment response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '출하 삭제에 실패했습니다.',
|
||||
};
|
||||
return { success: false, error: result.message || '출하 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] deleteShipment error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,49 +643,36 @@ export async function getLotOptions(): Promise<{
|
||||
success: boolean;
|
||||
data: LotOption[];
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/lots`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ShipmentActions] GET lot options error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[ShipmentActions] GET lot options error:', response?.status);
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: result.message || 'LOT 옵션 조회에 실패했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: result.message || 'LOT 옵션 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data || [],
|
||||
};
|
||||
return { success: true, data: result.data || [] };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] getLotOptions error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,49 +681,36 @@ export async function getLogisticsOptions(): Promise<{
|
||||
success: boolean;
|
||||
data: LogisticsOption[];
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/logistics`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ShipmentActions] GET logistics options error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[ShipmentActions] GET logistics options error:', response?.status);
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: result.message || '물류사 옵션 조회에 실패했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: result.message || '물류사 옵션 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data || [],
|
||||
};
|
||||
return { success: true, data: result.data || [] };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] getLogisticsOptions error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,48 +719,35 @@ export async function getVehicleTonnageOptions(): Promise<{
|
||||
success: boolean;
|
||||
data: VehicleTonnageOption[];
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/vehicle-tonnage`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ShipmentActions] GET vehicle tonnage options error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[ShipmentActions] GET vehicle tonnage options error:', response?.status);
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: result.message || '차량 톤수 옵션 조회에 실패했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: result.message || '차량 톤수 옵션 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data || [],
|
||||
};
|
||||
return { success: true, data: result.data || [] };
|
||||
} catch (error) {
|
||||
console.error('[ShipmentActions] getVehicleTonnageOptions error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { PricingData, ItemInfo } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -83,21 +83,6 @@ interface PriceApiData {
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 데이터 → 프론트엔드 타입 변환
|
||||
*/
|
||||
@@ -178,17 +163,24 @@ function transformFrontendToApi(data: PricingData): Record<string, unknown> {
|
||||
*/
|
||||
export async function getPricingById(id: string): Promise<PricingData | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('[PricingActions] GET pricing error:', error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
console.error('[PricingActions] GET pricing: 응답이 없습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PricingActions] GET pricing error:', response.status);
|
||||
return null;
|
||||
@@ -215,17 +207,24 @@ export async function getPricingById(id: string): Promise<PricingData | null> {
|
||||
*/
|
||||
export async function getItemInfo(itemId: string): Promise<ItemInfo | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items/${itemId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('[PricingActions] getItemInfo error:', error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
console.error('[PricingActions] getItemInfo: 응답이 없습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PricingActions] Item not found:', itemId);
|
||||
return null;
|
||||
@@ -257,22 +256,35 @@ export async function getItemInfo(itemId: string): Promise<ItemInfo | null> {
|
||||
*/
|
||||
export async function createPricing(
|
||||
data: PricingData
|
||||
): Promise<{ success: boolean; data?: PricingData; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[PricingActions] POST pricing request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '단가 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PricingActions] POST pricing response:', result);
|
||||
|
||||
@@ -303,9 +315,8 @@ export async function updatePricing(
|
||||
id: string,
|
||||
data: PricingData,
|
||||
changeReason?: string
|
||||
): Promise<{ success: boolean; data?: PricingData; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = {
|
||||
...transformFrontendToApi(data),
|
||||
change_reason: changeReason || null,
|
||||
@@ -313,15 +324,29 @@ export async function updatePricing(
|
||||
|
||||
console.log('[PricingActions] PUT pricing request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '단가 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PricingActions] PUT pricing response:', result);
|
||||
|
||||
@@ -348,18 +373,30 @@ export async function updatePricing(
|
||||
/**
|
||||
* 단가 삭제
|
||||
*/
|
||||
export async function deletePricing(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deletePricing(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '단가 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PricingActions] DELETE pricing response:', result);
|
||||
|
||||
@@ -383,18 +420,30 @@ export async function deletePricing(id: string): Promise<{ success: boolean; err
|
||||
/**
|
||||
* 단가 확정
|
||||
*/
|
||||
export async function finalizePricing(id: string): Promise<{ success: boolean; data?: PricingData; error?: string }> {
|
||||
export async function finalizePricing(id: string): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}/finalize`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '단가 확정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PricingActions] POST finalize response:', result);
|
||||
|
||||
@@ -432,19 +481,32 @@ export async function getPricingRevisions(priceId: string): Promise<{
|
||||
afterSnapshot: Record<string, unknown>;
|
||||
}>;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${priceId}/revisions`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '이력 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PricingActions] GET revisions response:', result);
|
||||
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Process, ProcessFormData, ClassificationRule } from '@/types/process';
|
||||
|
||||
// ============================================================================
|
||||
// API 헤더 생성
|
||||
// ============================================================================
|
||||
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API 타입 정의
|
||||
// ============================================================================
|
||||
@@ -144,9 +128,8 @@ export async function getProcessList(params?: {
|
||||
q?: string;
|
||||
status?: string;
|
||||
process_type?: string;
|
||||
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string }> {
|
||||
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -155,11 +138,19 @@ export async function getProcessList(params?: {
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.process_type) searchParams.set('process_type', params.process_type);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.API_URL}/v1/processes?${searchParams.toString()}`,
|
||||
{ method: 'GET', headers, cache: 'no-store' }
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<ApiProcess>> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -184,16 +175,21 @@ export async function getProcessList(params?: {
|
||||
/**
|
||||
* 공정 상세 조회
|
||||
*/
|
||||
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string }> {
|
||||
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -210,17 +206,23 @@ export async function getProcessById(id: string): Promise<{ success: boolean; da
|
||||
/**
|
||||
* 공정 생성
|
||||
*/
|
||||
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string }> {
|
||||
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -237,17 +239,23 @@ export async function createProcess(data: ProcessFormData): Promise<{ success: b
|
||||
/**
|
||||
* 공정 수정
|
||||
*/
|
||||
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string }> {
|
||||
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -264,15 +272,20 @@ export async function updateProcess(id: string, data: ProcessFormData): Promise<
|
||||
/**
|
||||
* 공정 삭제
|
||||
*/
|
||||
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<null> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -289,16 +302,21 @@ export async function deleteProcess(id: string): Promise<{ success: boolean; err
|
||||
/**
|
||||
* 공정 일괄 삭제
|
||||
*/
|
||||
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
|
||||
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
body: JSON.stringify({ ids: ids.map((id) => parseInt(id, 10)) }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{ deleted_count: number }> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -315,15 +333,20 @@ export async function deleteProcesses(ids: string[]): Promise<{ success: boolean
|
||||
/**
|
||||
* 공정 상태 토글
|
||||
*/
|
||||
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string }> {
|
||||
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}/toggle`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}/toggle`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -344,16 +367,22 @@ export async function getProcessOptions(): Promise<{
|
||||
success: boolean;
|
||||
data?: Array<{ id: string; processCode: string; processName: string; processType: string; department: string }>;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes/options`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/options`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '옵션 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Array<{ id: number; process_code: string; process_name: string; process_type: string; department: string }>> =
|
||||
await response.json();
|
||||
|
||||
@@ -384,16 +413,22 @@ export async function getProcessStats(): Promise<{
|
||||
success: boolean;
|
||||
data?: { total: number; active: number; inactive: number; byType: Record<string, number> };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(`${process.env.API_URL}/v1/processes/stats`, {
|
||||
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/stats`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{ total: number; active: number; inactive: number; by_type: Record<string, number> }> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
/**
|
||||
* 생산 현황판 서버 액션
|
||||
* API 연동 완료 (2025-12-26)
|
||||
* serverFetch 마이그레이션 (2025-12-30)
|
||||
*/
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { WorkOrder, WorkerStatus, ProcessType, DashboardStats } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 타입 =====
|
||||
interface WorkOrderApiItem {
|
||||
id: number;
|
||||
@@ -101,9 +89,14 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
|
||||
stats: DashboardStats;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const emptyResult = {
|
||||
success: false,
|
||||
workOrders: [] as WorkOrder[],
|
||||
workerStatus: [] as WorkerStatus[],
|
||||
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
|
||||
};
|
||||
|
||||
try {
|
||||
// 작업지시 목록 조회
|
||||
const params = new URLSearchParams({ per_page: '100' });
|
||||
if (processType && processType !== 'all') {
|
||||
@@ -113,19 +106,21 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?${params.toString()}`;
|
||||
console.log('[ProductionDashboardActions] GET:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
// serverFetch handles 401 with redirect, so we only check for other errors
|
||||
if (error || !response) {
|
||||
console.warn('[ProductionDashboardActions] GET error:', error?.message);
|
||||
return {
|
||||
...emptyResult,
|
||||
error: error?.message || '데이터 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ProductionDashboardActions] GET error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
workOrders: [],
|
||||
workerStatus: [],
|
||||
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
|
||||
...emptyResult,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
}
|
||||
@@ -134,10 +129,7 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
workOrders: [],
|
||||
workerStatus: [],
|
||||
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
|
||||
...emptyResult,
|
||||
error: result.message || '데이터 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
@@ -192,10 +184,7 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
|
||||
} catch (error) {
|
||||
console.error('[ProductionDashboardActions] getDashboardData error:', error);
|
||||
return {
|
||||
success: false,
|
||||
workOrders: [],
|
||||
workerStatus: [],
|
||||
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
|
||||
...emptyResult,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
WorkOrder,
|
||||
WorkOrderStats,
|
||||
@@ -32,19 +32,6 @@ import {
|
||||
transformStatsApiToFrontend,
|
||||
} from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
@@ -68,8 +55,13 @@ export async function getWorkOrders(params?: {
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
}> {
|
||||
const emptyResponse = {
|
||||
success: false,
|
||||
data: [] as WorkOrder[],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
};
|
||||
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -89,29 +81,22 @@ export async function getWorkOrders(params?: {
|
||||
|
||||
console.log('[WorkOrderActions] GET work-orders:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { ...emptyResponse, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkOrderActions] GET work-orders error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
return { ...emptyResponse, error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
...emptyResponse,
|
||||
error: result.message || '작업지시 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
@@ -138,12 +123,7 @@ export async function getWorkOrders(params?: {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] getWorkOrders error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { ...emptyResponse, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,23 +134,19 @@ export async function getWorkOrderStats(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/stats`;
|
||||
|
||||
console.log('[WorkOrderActions] GET stats:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkOrderActions] GET stats error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
return { success: false, error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -190,10 +166,7 @@ export async function getWorkOrderStats(): Promise<{
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] getWorkOrderStats error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,23 +177,19 @@ export async function getWorkOrderById(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`;
|
||||
|
||||
console.log('[WorkOrderActions] GET work-order:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[WorkOrderActions] GET work-order error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
return { success: false, error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -238,10 +207,7 @@ export async function getWorkOrderById(id: string): Promise<{
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] getWorkOrderById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +220,6 @@ export async function createWorkOrder(
|
||||
}
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = {
|
||||
...transformFrontendToApi(data),
|
||||
sales_order_id: data.salesOrderId,
|
||||
@@ -264,15 +229,18 @@ export async function createWorkOrder(
|
||||
|
||||
console.log('[WorkOrderActions] POST work-order request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] POST work-order response:', result);
|
||||
|
||||
@@ -289,10 +257,7 @@ export async function createWorkOrder(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] createWorkOrder error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,20 +267,22 @@ export async function updateWorkOrder(
|
||||
data: Partial<WorkOrder>
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[WorkOrderActions] PUT work-order request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PUT work-order response:', result);
|
||||
|
||||
@@ -332,26 +299,22 @@ export async function updateWorkOrder(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] updateWorkOrder error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 작업지시 삭제 =====
|
||||
export async function deleteWorkOrder(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] DELETE work-order response:', result);
|
||||
|
||||
@@ -365,10 +328,7 @@ export async function deleteWorkOrder(id: string): Promise<{ success: boolean; e
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] deleteWorkOrder error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,19 +338,20 @@ export async function updateWorkOrderStatus(
|
||||
status: WorkOrderStatus
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
console.log('[WorkOrderActions] PATCH status request:', { status });
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({ status }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH status response:', result);
|
||||
|
||||
@@ -407,10 +368,7 @@ export async function updateWorkOrderStatus(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] updateWorkOrderStatus error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,22 +379,23 @@ export async function assignWorkOrder(
|
||||
teamId?: number
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const body: { assignee_id: number; team_id?: number } = { assignee_id: assigneeId };
|
||||
if (teamId) body.team_id = teamId;
|
||||
|
||||
console.log('[WorkOrderActions] PATCH assign request:', body);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/assign`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH assign response:', result);
|
||||
|
||||
@@ -453,10 +412,7 @@ export async function assignWorkOrder(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] assignWorkOrder error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,19 +422,20 @@ export async function toggleBendingField(
|
||||
field: string
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
console.log('[WorkOrderActions] PATCH bending toggle request:', { field });
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/bending/toggle`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({ field }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH bending toggle response:', result);
|
||||
|
||||
@@ -495,10 +452,7 @@ export async function toggleBendingField(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] toggleBendingField error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,19 +466,20 @@ export async function addWorkOrderIssue(
|
||||
}
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
console.log('[WorkOrderActions] POST issue request:', data);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/issues`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] POST issue response:', result);
|
||||
|
||||
@@ -541,10 +496,7 @@ export async function addWorkOrderIssue(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] addWorkOrderIssue error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,18 +506,17 @@ export async function resolveWorkOrderIssue(
|
||||
issueId: string
|
||||
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
console.log('[WorkOrderActions] PATCH issue resolve:', { workOrderId, issueId });
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
}
|
||||
{ method: 'PATCH' }
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkOrderActions] PATCH issue resolve response:', result);
|
||||
|
||||
@@ -582,10 +533,7 @@ export async function resolveWorkOrderIssue(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] resolveWorkOrderIssue error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +558,6 @@ export async function getSalesOrdersForWorkOrder(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
// 작업지시 생성 가능한 상태만 조회 (예: 회계확인 완료)
|
||||
@@ -623,19 +570,15 @@ export async function getSalesOrdersForWorkOrder(params?: {
|
||||
|
||||
console.log('[WorkOrderActions] GET sales-orders for work-order:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkOrderActions] GET sales-orders error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -677,11 +620,7 @@ export async function getSalesOrdersForWorkOrder(params?: {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] getSalesOrdersForWorkOrder error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,24 +645,19 @@ export async function getDepartmentsWithUsers(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree?with_users=1`;
|
||||
|
||||
console.log('[WorkOrderActions] GET departments with users:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkOrderActions] GET departments error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
return { success: false, data: [], error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -765,10 +699,6 @@ export async function getDepartmentsWithUsers(): Promise<{
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderActions] getDepartmentsWithUsers error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* - PATCH /api/v1/work-results/{id}/packaging - 포장 상태 토글
|
||||
*/
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { ProcessType } from '../WorkOrders/types';
|
||||
import type {
|
||||
WorkResult,
|
||||
@@ -28,19 +28,6 @@ import {
|
||||
transformStatsApiToFrontend,
|
||||
} from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
@@ -68,7 +55,6 @@ export async function getWorkResults(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -93,12 +79,20 @@ export async function getWorkResults(params?: {
|
||||
|
||||
console.log('[WorkResultActions] GET work-results:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
console.warn('[WorkResultActions] GET work-results error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkResultActions] GET work-results error:', response.status);
|
||||
return {
|
||||
@@ -160,7 +154,6 @@ export async function getWorkResultStats(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.workDateFrom) searchParams.set('work_date_from', params.workDateFrom);
|
||||
@@ -174,12 +167,18 @@ export async function getWorkResultStats(params?: {
|
||||
|
||||
console.log('[WorkResultActions] GET stats:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
console.warn('[WorkResultActions] GET stats error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkResultActions] GET stats error:', response.status);
|
||||
return {
|
||||
@@ -219,17 +218,22 @@ export async function getWorkResultById(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}`;
|
||||
|
||||
console.log('[WorkResultActions] GET work-result:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[WorkResultActions] GET work-result error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[WorkResultActions] GET work-result error:', response.status);
|
||||
return {
|
||||
@@ -281,8 +285,6 @@ export async function createWorkResult(data: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const apiData: Record<string, unknown> = {
|
||||
work_order_id: data.workOrderId,
|
||||
lot_no: data.lotNo,
|
||||
@@ -302,15 +304,22 @@ export async function createWorkResult(data: {
|
||||
|
||||
console.log('[WorkResultActions] POST work-result request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[WorkResultActions] POST work-result error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkResultActions] POST work-result response:', result);
|
||||
|
||||
@@ -344,20 +353,26 @@ export async function updateWorkResult(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[WorkResultActions] PUT work-result request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[WorkResultActions] PUT work-result error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkResultActions] PUT work-result response:', result);
|
||||
|
||||
@@ -387,16 +402,21 @@ export async function deleteWorkResult(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[WorkResultActions] DELETE work-result error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkResultActions] DELETE work-result response:', result);
|
||||
|
||||
@@ -424,18 +444,23 @@ export async function toggleInspection(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
console.log('[WorkResultActions] PATCH inspection toggle:', id);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}/inspection`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[WorkResultActions] PATCH inspection error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkResultActions] PATCH inspection response:', result);
|
||||
|
||||
@@ -466,18 +491,23 @@ export async function togglePackaging(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
console.log('[WorkResultActions] PATCH packaging toggle:', id);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}/packaging`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[WorkResultActions] PATCH packaging error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkResultActions] PATCH packaging response:', result);
|
||||
|
||||
|
||||
@@ -7,22 +7,9 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 타입 =====
|
||||
interface WorkOrderApiItem {
|
||||
id: number;
|
||||
@@ -97,18 +84,21 @@ export async function getMyWorkOrders(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
// 작업 대기 + 작업중 상태만 조회 (완료 제외)
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?per_page=100&assigned_to_me=1`;
|
||||
|
||||
console.log('[WorkerScreenActions] GET my work orders:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
console.warn('[WorkerScreenActions] GET error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: error?.message || '네트워크 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkerScreenActions] GET error:', response.status);
|
||||
@@ -156,14 +146,11 @@ export async function completeWorkOrder(
|
||||
materials?: { materialId: number; quantity: number; lotNo?: string }[]
|
||||
): Promise<{ success: boolean; lotNo?: string; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
// 상태를 completed로 변경
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
status: 'completed',
|
||||
materials,
|
||||
@@ -171,6 +158,13 @@ export async function completeWorkOrder(
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '네트워크 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkerScreenActions] Complete response:', result);
|
||||
|
||||
@@ -215,18 +209,21 @@ export async function getMaterialsForWorkOrder(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
// 작업지시 BOM 기준 자재 목록 조회
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/materials`;
|
||||
|
||||
console.log('[WorkerScreenActions] GET materials for work order:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
console.warn('[WorkerScreenActions] GET materials error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: error?.message || '네트워크 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkerScreenActions] GET materials error:', response.status);
|
||||
@@ -284,17 +281,21 @@ export async function registerMaterialInput(
|
||||
materialIds: number[]
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/material-inputs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ material_ids: materialIds }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '네트워크 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkerScreenActions] Register material input response:', result);
|
||||
|
||||
@@ -325,17 +326,21 @@ export async function reportIssue(
|
||||
}
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '네트워크 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkerScreenActions] Report issue response:', result);
|
||||
|
||||
@@ -385,17 +390,20 @@ export async function getProcessSteps(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps`;
|
||||
|
||||
console.log('[WorkerScreenActions] GET process steps:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
console.warn('[WorkerScreenActions] GET process steps error:', error?.message);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: error?.message || '네트워크 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkerScreenActions] GET process steps error:', response.status);
|
||||
@@ -471,17 +479,21 @@ export async function requestInspection(
|
||||
stepId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps/${stepId}/inspection-request`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || '네트워크 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[WorkerScreenActions] Inspection request response:', result);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
/**
|
||||
* 검사 관리 Server Actions
|
||||
* API 연동 완료 (2025-12-26)
|
||||
* fetch-wrapper 마이그레이션 완료 (2025-12-30)
|
||||
*
|
||||
* API Endpoints:
|
||||
* - GET /api/v1/inspections - 목록 조회
|
||||
@@ -14,7 +15,7 @@
|
||||
* - PATCH /api/v1/inspections/{id}/complete - 검사 완료 처리
|
||||
*/
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
Inspection,
|
||||
InspectionStats,
|
||||
@@ -23,19 +24,6 @@ import type {
|
||||
InspectionItem,
|
||||
} from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 타입 =====
|
||||
interface InspectionApiItem {
|
||||
id: number;
|
||||
@@ -190,9 +178,9 @@ export async function getInspections(params?: {
|
||||
data: Inspection[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -212,12 +200,29 @@ export async function getInspections(params?: {
|
||||
|
||||
console.log('[InspectionActions] GET inspections:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '검사 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[InspectionActions] GET inspections error:', response.status);
|
||||
return {
|
||||
@@ -277,9 +282,9 @@ export async function getInspectionStats(params?: {
|
||||
success: boolean;
|
||||
data?: InspectionStats;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
|
||||
@@ -293,12 +298,25 @@ export async function getInspectionStats(params?: {
|
||||
|
||||
console.log('[InspectionActions] GET stats:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '통계 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[InspectionActions] GET stats error:', response.status);
|
||||
return {
|
||||
@@ -341,19 +359,32 @@ export async function getInspectionById(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: Inspection;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`;
|
||||
|
||||
console.log('[InspectionActions] GET inspection:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '검사 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[InspectionActions] GET inspection error:', response.status);
|
||||
return {
|
||||
@@ -399,10 +430,9 @@ export async function createInspection(data: {
|
||||
success: boolean;
|
||||
data?: Inspection;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const apiData: Record<string, unknown> = {
|
||||
inspection_type: data.inspectionType,
|
||||
lot_no: data.lotNo,
|
||||
@@ -425,15 +455,29 @@ export async function createInspection(data: {
|
||||
|
||||
console.log('[InspectionActions] POST inspection request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '검사 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[InspectionActions] POST inspection response:', result);
|
||||
|
||||
@@ -470,10 +514,9 @@ export async function updateInspection(
|
||||
success: boolean;
|
||||
data?: Inspection;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const apiData: Record<string, unknown> = {};
|
||||
|
||||
if (data.items) {
|
||||
@@ -503,15 +546,29 @@ export async function updateInspection(
|
||||
|
||||
console.log('[InspectionActions] PUT inspection request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '검사 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[InspectionActions] PUT inspection response:', result);
|
||||
|
||||
@@ -539,18 +596,31 @@ export async function updateInspection(
|
||||
export async function deleteInspection(id: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '검사 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[InspectionActions] DELETE inspection response:', result);
|
||||
|
||||
@@ -582,10 +652,9 @@ export async function completeInspection(
|
||||
success: boolean;
|
||||
data?: Inspection;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const apiData = {
|
||||
result: data.result === '합격' ? 'pass' : 'fail',
|
||||
opinion: data.opinion,
|
||||
@@ -593,15 +662,29 @@ export async function completeInspection(
|
||||
|
||||
console.log('[InspectionActions] PATCH complete request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}/complete`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '검사 완료 처리에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[InspectionActions] PATCH complete response:', result);
|
||||
|
||||
@@ -623,4 +706,4 @@ export async function completeInspection(
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
Quote,
|
||||
QuoteApiData,
|
||||
@@ -30,19 +30,6 @@ import type {
|
||||
} from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
export interface PaginationMeta {
|
||||
currentPage: number;
|
||||
@@ -57,9 +44,9 @@ export async function getQuotes(params?: QuoteListParams): Promise<{
|
||||
data: Quote[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -78,12 +65,29 @@ export async function getQuotes(params?: QuoteListParams): Promise<{
|
||||
|
||||
console.log('[QuoteActions] GET quotes:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '견적 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[QuoteActions] GET quotes error:', response.status);
|
||||
return {
|
||||
@@ -142,18 +146,29 @@ export async function getQuoteById(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: Quote;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[QuoteActions] GET quote error:', response.status);
|
||||
@@ -188,21 +203,33 @@ export async function getQuoteById(id: string): Promise<{
|
||||
// ===== 견적 등록 =====
|
||||
export async function createQuote(
|
||||
data: Partial<Quote>
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[QuoteActions] POST quote request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] POST quote response:', result);
|
||||
@@ -231,21 +258,33 @@ export async function createQuote(
|
||||
export async function updateQuote(
|
||||
id: string,
|
||||
data: Partial<Quote>
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[QuoteActions] PUT quote request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] PUT quote response:', result);
|
||||
@@ -271,17 +310,28 @@ export async function updateQuote(
|
||||
}
|
||||
|
||||
// ===== 견적 삭제 =====
|
||||
export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] DELETE quote response:', result);
|
||||
@@ -304,18 +354,29 @@ export async function deleteQuote(id: string): Promise<{ success: boolean; error
|
||||
}
|
||||
|
||||
// ===== 견적 일괄 삭제 =====
|
||||
export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/bulk`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/bulk`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적 일괄 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] BULK DELETE quotes response:', result);
|
||||
@@ -342,17 +403,29 @@ export async function finalizeQuote(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: Quote;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/finalize`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/finalize`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적 확정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] POST finalize response:', result);
|
||||
@@ -382,17 +455,29 @@ export async function cancelFinalizeQuote(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: Quote;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/cancel-finalize`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/cancel-finalize`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적 확정 취소에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] POST cancel-finalize response:', result);
|
||||
@@ -423,17 +508,29 @@ export async function convertQuoteToOrder(id: string): Promise<{
|
||||
data?: Quote;
|
||||
orderId?: string;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/convert`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/convert`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '수주 전환에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] POST convert response:', result);
|
||||
@@ -464,18 +561,29 @@ export async function getQuoteNumberPreview(): Promise<{
|
||||
success: boolean;
|
||||
data?: string;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/number/preview`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/number/preview`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '견적번호 미리보기에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
@@ -504,17 +612,29 @@ export async function generateQuotePdf(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: Blob;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/pdf`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/pdf`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'PDF 생성에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
@@ -542,18 +662,29 @@ export async function generateQuotePdf(id: string): Promise<{
|
||||
export async function sendQuoteEmail(
|
||||
id: string,
|
||||
emailData: { email: string; subject?: string; message?: string }
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/email`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/email`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(emailData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(emailData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '이메일 발송에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] POST send email response:', result);
|
||||
@@ -579,18 +710,29 @@ export async function sendQuoteEmail(
|
||||
export async function sendQuoteKakao(
|
||||
id: string,
|
||||
kakaoData: { phone: string; templateId?: string }
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/kakao`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/kakao`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(kakaoData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(kakaoData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '카카오 발송에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] POST send kakao response:', result);
|
||||
@@ -630,6 +772,7 @@ export async function getQuotesSummary(params?: {
|
||||
conversionRate: number;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
// 목록 조회를 통해 통계 계산 (별도 API 없는 경우)
|
||||
@@ -643,6 +786,7 @@ export async function getQuotesSummary(params?: {
|
||||
return {
|
||||
success: false,
|
||||
error: listResult.error,
|
||||
__authError: listResult.__authError,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { ComprehensiveAnalysisData } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface TodayIssueItemApi {
|
||||
id: string;
|
||||
@@ -139,9 +126,9 @@ export async function getComprehensiveAnalysis(params?: {
|
||||
success: boolean;
|
||||
data?: ComprehensiveAnalysisData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
@@ -149,23 +136,22 @@ export async function getComprehensiveAnalysis(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/comprehensive-analysis${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ComprehensiveAnalysisActions] GET error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '종합 분석 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '종합 분석 조회에 실패했습니다.',
|
||||
@@ -191,27 +177,26 @@ export async function getComprehensiveAnalysis(params?: {
|
||||
export async function approveIssue(issueId: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${issueId}/approve`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ComprehensiveAnalysisActions] POST approve error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '승인에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '승인에 실패했습니다.',
|
||||
@@ -232,28 +217,27 @@ export async function approveIssue(issueId: string): Promise<{
|
||||
export async function rejectIssue(issueId: string, reason?: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${issueId}/reject`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ comment: reason }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ComprehensiveAnalysisActions] POST reject error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '반려에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '반려에 실패했습니다.',
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
// ===== 계정 탈퇴 =====
|
||||
export async function withdrawAccount(): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/users/withdraw`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '계정 탈퇴에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -55,19 +55,32 @@ export async function withdrawAccount(): Promise<{
|
||||
export async function suspendTenant(): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants/suspend`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '사용 중지에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Account, AccountFormData, AccountStatus } from './types';
|
||||
import { BANK_LABELS } from './types';
|
||||
|
||||
@@ -39,19 +39,6 @@ interface ApiSingleResponse {
|
||||
data: BankAccountApiData;
|
||||
}
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 데이터 변환: API → Frontend =====
|
||||
function transformApiToFrontend(apiData: BankAccountApiData): Account {
|
||||
return {
|
||||
@@ -91,9 +78,9 @@ export async function getBankAccounts(params?: {
|
||||
data?: Account[];
|
||||
meta?: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', params.page.toString());
|
||||
@@ -102,12 +89,23 @@ export async function getBankAccounts(params?: {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiListResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -127,18 +125,29 @@ export async function getBankAccount(id: number): Promise<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -158,20 +167,31 @@ export async function createBankAccount(data: AccountFormData): Promise<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -194,20 +214,31 @@ export async function updateBankAccount(
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -226,17 +257,28 @@ export async function updateBankAccount(
|
||||
export async function deleteBankAccount(id: number): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '계좌 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -255,17 +297,28 @@ export async function toggleBankAccountStatus(id: number): Promise<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}/toggle`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -285,17 +338,28 @@ export async function setPrimaryBankAccount(id: number): Promise<{
|
||||
success: boolean;
|
||||
data?: Account;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}/set-primary`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '대표 계좌 설정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiSingleResponse = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://sam.kr:8080';
|
||||
|
||||
// ===== API Helper =====
|
||||
async function getAuthHeaders() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
};
|
||||
}
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
// ===== 타입 정의 =====
|
||||
|
||||
@@ -79,21 +65,30 @@ export async function getAttendanceSetting(): Promise<{
|
||||
success: boolean;
|
||||
data?: AttendanceSettingFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getAuthHeaders();
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/attendance`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.message || `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
const errorData = await response?.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.message || `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,21 +116,30 @@ export async function updateAttendanceSetting(
|
||||
success: boolean;
|
||||
data?: AttendanceSettingFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getAuthHeaders();
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/attendance`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(transformToApi(data)),
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(transformToApi(data)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.message || `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
const errorData = await response?.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.message || `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { CompanyFormData } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -32,19 +32,6 @@ interface TenantApiData {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// API 헤더 생성
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 정보 조회
|
||||
*/
|
||||
@@ -52,18 +39,29 @@ export async function getCompanyInfo(): Promise<{
|
||||
success: boolean;
|
||||
data?: CompanyFormData & { tenantId: number };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '회사 정보 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -90,20 +88,31 @@ export async function updateCompanyInfo(
|
||||
success: boolean;
|
||||
data?: CompanyFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(tenantId, data);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '회사 정보 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { LeavePolicySettings } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface LeavePolicyApi {
|
||||
id: number;
|
||||
@@ -70,22 +57,29 @@ export async function getLeavePolicy(): Promise<{
|
||||
success: boolean;
|
||||
data?: LeavePolicySettings;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[LeavePolicyActions] GET error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[LeavePolicyActions] GET error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,24 +112,30 @@ export async function updateLeavePolicy(data: Partial<LeavePolicySettings>): Pro
|
||||
success: boolean;
|
||||
data?: LeavePolicySettings;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
|
||||
|
||||
const apiData = transformToApi(data);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[LeavePolicyActions] PUT error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[LeavePolicyActions] PUT error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { NotificationSettings } from './types';
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS } from './types';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 알림 설정 조회 =====
|
||||
export async function getNotificationSettings(): Promise<{
|
||||
success: boolean;
|
||||
data: NotificationSettings;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/notifications`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[NotificationActions] GET settings error:', response.status);
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: DEFAULT_NOTIFICATION_SETTINGS,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.warn('[NotificationActions] GET settings error:', response?.status);
|
||||
return {
|
||||
success: true,
|
||||
data: DEFAULT_NOTIFICATION_SETTINGS,
|
||||
@@ -69,20 +63,33 @@ export async function getNotificationSettings(): Promise<{
|
||||
// ===== 알림 설정 저장 =====
|
||||
export async function saveNotificationSettings(
|
||||
settings: NotificationSettings
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(settings);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/notifications`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '알림 설정 저장에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { PaymentApiData, PaymentHistory } from './types';
|
||||
import { transformApiToFrontend } from './utils';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 결제 목록 조회 =====
|
||||
export async function getPayments(params?: {
|
||||
page?: number;
|
||||
@@ -35,10 +22,9 @@ export async function getPayments(params?: {
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
// 쿼리 파라미터 생성
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.append('page', String(params.page));
|
||||
@@ -51,12 +37,30 @@ export async function getPayments(params?: {
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
||||
error: '결제 내역을 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -144,19 +148,32 @@ export async function getPaymentStatement(id: string): Promise<{
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments/${id}/statement`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '명세서를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Popup, PopupFormData } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi, type PopupApiData } from './utils';
|
||||
|
||||
@@ -25,25 +25,6 @@ interface ApiResponse<T> {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API 헤더 생성
|
||||
*/
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
@@ -65,7 +46,6 @@ export async function getPopups(params?: {
|
||||
status?: string;
|
||||
}): Promise<Popup[]> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
@@ -76,12 +56,16 @@ export async function getPopups(params?: {
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[PopupActions] GET list error:', error?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PopupActions] GET list error:', response.status);
|
||||
return [];
|
||||
@@ -106,17 +90,19 @@ export async function getPopups(params?: {
|
||||
*/
|
||||
export async function getPopupById(id: string): Promise<Popup | null> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[PopupActions] GET popup error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[PopupActions] GET popup error:', response.status);
|
||||
return null;
|
||||
@@ -140,22 +126,35 @@ export async function getPopupById(id: string): Promise<Popup | null> {
|
||||
*/
|
||||
export async function createPopup(
|
||||
data: PopupFormData
|
||||
): Promise<{ success: boolean; data?: Popup; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Popup; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[PopupActions] POST popup request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '팝업 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PopupActions] POST popup response:', result);
|
||||
|
||||
@@ -185,22 +184,35 @@ export async function createPopup(
|
||||
export async function updatePopup(
|
||||
id: string,
|
||||
data: PopupFormData
|
||||
): Promise<{ success: boolean; data?: Popup; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Popup; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[PopupActions] PUT popup request:', apiData);
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '팝업 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PopupActions] PUT popup response:', result);
|
||||
|
||||
@@ -227,18 +239,30 @@ export async function updatePopup(
|
||||
/**
|
||||
* 팝업 삭제
|
||||
*/
|
||||
export async function deletePopup(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deletePopup(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '팝업 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[PopupActions] DELETE popup response:', result);
|
||||
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { SubscriptionApiData, UsageApiData, SubscriptionInfo } from './types';
|
||||
import { transformApiToFrontend } from './utils';
|
||||
|
||||
// ===== API 헤더 생성 =====
|
||||
async function getApiHeaders(): Promise<HeadersInit> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 현재 활성 구독 조회 =====
|
||||
export async function getCurrentSubscription(): Promise<{
|
||||
success: boolean;
|
||||
data: SubscriptionApiData | null;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/current`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '구독 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -64,19 +66,34 @@ export async function getUsage(): Promise<{
|
||||
success: boolean;
|
||||
data: UsageApiData | null;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/usage`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '사용량 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -108,19 +125,32 @@ export async function cancelSubscription(
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/${id}/cancel`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ reason }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '구독 취소에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
@@ -147,19 +177,32 @@ export async function requestDataExport(
|
||||
success: boolean;
|
||||
data?: { id: number; status: string };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/export`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ export_type: exportType }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
error: '내보내기 요청에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://sam.kr:8080';
|
||||
|
||||
// ===== API Helper =====
|
||||
async function getAuthHeaders() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 타입 정의 =====
|
||||
|
||||
// API 응답 타입
|
||||
@@ -99,21 +87,27 @@ export async function getWorkSetting(): Promise<{
|
||||
success: boolean;
|
||||
data?: WorkSettingFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getAuthHeaders();
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/settings/work`, {
|
||||
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.message || `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
const errorData = await response?.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.message || `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,21 +135,27 @@ export async function updateWorkSetting(
|
||||
success: boolean;
|
||||
data?: WorkSettingFormData;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const headers = await getAuthHeaders();
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/settings/work`, {
|
||||
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(transformToApi(data)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.message || `API 오류: ${response.status}`,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
const errorData = await response?.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData?.message || `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user