Skip to content

Commit 04aceb5

Browse files
authored
Merge branch 'main' into 48-refactor/admin-refactor
2 parents 9a852c7 + 2661605 commit 04aceb5

17 files changed

Lines changed: 778 additions & 92 deletions

File tree

apps/web/src/api/analyze.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { type RiskDetectAnalyzeResponse, type RiskSaveBody } from '@/app/(main)/analysis/_types/analysis.types';
2+
import { client } from './client';
3+
4+
const isDev = process.env.NODE_ENV === 'development';
5+
6+
export const analyzeRisk = async (formData: FormData, signal?: AbortSignal): Promise<RiskDetectAnalyzeResponse> => {
7+
if (isDev) {
8+
console.group('[진단] analyzeRisk — POST /api/v1/risk-detector/analyze');
9+
for (const [key, value] of formData.entries()) {
10+
if (value instanceof File) {
11+
console.log(` ${key}: File { name: "${value.name}", type: "${value.type}", size: ${value.size} bytes }`);
12+
} else {
13+
console.log(` ${key}: ${value}`);
14+
}
15+
}
16+
}
17+
18+
const res = await client
19+
.post('api/v1/risk-detector/analyze', {
20+
body: formData,
21+
headers: { 'content-type': undefined },
22+
timeout: 180000,
23+
signal,
24+
})
25+
.json<RiskDetectAnalyzeResponse>();
26+
27+
if (isDev) {
28+
console.log('[응답]', res);
29+
console.groupEnd();
30+
}
31+
return res;
32+
};
33+
34+
export const saveRisk = async (body: RiskSaveBody, userId: number): Promise<{ id: string }> => {
35+
if (isDev) {
36+
console.group('[진단] saveRisk — POST /api/v1/risk-detector/save');
37+
console.log('[요청]', { userId });
38+
}
39+
40+
const res = await client
41+
.post('api/v1/risk-detector/save', { json: body, headers: { 'x-user-id': String(userId) } })
42+
.json<{ id: string }>();
43+
44+
if (isDev) {
45+
console.log('[응답]', res);
46+
console.groupEnd();
47+
}
48+
return res;
49+
};
50+
51+
export const deleteRisk = async (id: string, userId: number): Promise<void> => {
52+
if (isDev) {
53+
console.group(`[진단] deleteRisk — DELETE /api/v1/risk-detector/${id}`);
54+
console.log('[요청]', { id, userId });
55+
}
56+
57+
await client.delete(`api/v1/risk-detector/${id}`, { headers: { 'x-user-id': String(userId) } });
58+
59+
if (isDev) {
60+
console.log('[응답] 204 No Content');
61+
console.groupEnd();
62+
}
63+
};

apps/web/src/api/client.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import ky, { type KyInstance } from 'ky';
2-
import { getClientApiBaseUrl } from './baseUrl';
2+
3+
const getApiBaseUrl = () => {
4+
// 브라우저에서는 Next.js 프록시 경유 — 쿠키를 현재 origin에 설정하기 위해
5+
if (typeof window !== 'undefined') {
6+
return window.location.origin;
7+
}
8+
const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
9+
if (!apiBaseUrl) {
10+
throw new Error('NEXT_PUBLIC_API_BASE_URL is not set');
11+
}
12+
return apiBaseUrl;
13+
};
314

415
const REFRESH_PATH = 'api/v1/users/refresh';
516
const PRE_AUTH_PATHS = ['api/v1/users/login', 'api/v1/users/signup', 'api/v1/auth/oauth', 'api/v1/auth/social/signup'];
@@ -11,7 +22,7 @@ let refreshPromise: Promise<Response> | null = null;
1122

1223
const triggerRefresh = () => {
1324
if (!refreshPromise) {
14-
refreshPromise = fetch(`${getClientApiBaseUrl()}/${REFRESH_PATH}`, {
25+
refreshPromise = fetch(`${getApiBaseUrl()}/${REFRESH_PATH}`, {
1526
method: 'POST',
1627
credentials: 'include',
1728
}).finally(() => {
@@ -23,7 +34,7 @@ const triggerRefresh = () => {
2334

2435
const createClient = () =>
2536
ky.create({
26-
prefixUrl: getClientApiBaseUrl(),
37+
prefixUrl: getApiBaseUrl(),
2738
timeout: 10000,
2839
credentials: 'include',
2940
headers: {

apps/web/src/api/files.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { type PresignedUrlResponse } from '@/app/(main)/analysis/_types/analysis.types';
2+
import { client } from './client';
3+
4+
const isDev = process.env.NODE_ENV === 'development';
5+
6+
export const getPresignedUrl = async (filename: string, contentType: string): Promise<PresignedUrlResponse> => {
7+
if (isDev) {
8+
console.group(`[진단] getPresignedUrl — ${filename}`);
9+
console.log('[요청]', { filename, contentType, type: 'Estimate' });
10+
}
11+
12+
const res = await client
13+
.get('api/v1/files/presigned-url', { searchParams: { type: 'Estimate', filename, contentType } })
14+
.json<PresignedUrlResponse>();
15+
16+
if (isDev) {
17+
console.log('[응답] presigned URL 발급 완료');
18+
console.groupEnd();
19+
}
20+
return res;
21+
};
22+
23+
export const uploadToS3 = async (uploadUrl: string, file: File): Promise<void> => {
24+
if (isDev) {
25+
console.group(`[진단] uploadToS3 — ${file.name}`);
26+
console.log('[요청]', { name: file.name, type: file.type, size: file.size });
27+
}
28+
29+
const res = await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file });
30+
31+
if (!res.ok) {
32+
const body = await res.text().catch(() => '');
33+
throw new Error(`S3 업로드 실패: ${res.status}${body ? ` — ${body}` : ''}`);
34+
}
35+
36+
if (isDev) {
37+
console.log('[응답]', { status: res.status, ok: res.ok });
38+
console.groupEnd();
39+
}
40+
};

apps/web/src/api/history/types.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1+
import { type RiskReport } from '@/app/(main)/analysis/_types/analysis.types';
12
import { type EstimateGenerateRequest, type EstimateGenerateResponse } from '@/app/(main)/estimate/_types/api';
23

3-
interface RiskSummary {
4-
total_risk_items: number;
5-
chips: { 누락: number; 중복: number; 불분명: number };
6-
}
7-
8-
interface RiskResult {
9-
report: {
10-
summary: RiskSummary;
11-
};
4+
export interface EstimateItem {
5+
id: string;
6+
user_id: string;
7+
created_at: string;
8+
input: EstimateGenerateRequest;
9+
result: EstimateGenerateResponse;
1210
}
1311

1412
interface RiskInput {
@@ -18,12 +16,8 @@ interface RiskInput {
1816
[key: string]: unknown;
1917
}
2018

21-
export interface EstimateItem {
22-
id: string;
23-
user_id: string;
24-
created_at: string;
25-
input: EstimateGenerateRequest;
26-
result: EstimateGenerateResponse;
19+
interface RiskResult {
20+
report: RiskReport;
2721
}
2822

2923
export interface RiskItem {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { vars } from '@muneo/design-system';
2+
import { keyframes, style } from '@vanilla-extract/css';
3+
4+
const float = keyframes({
5+
'0%, 100%': { transform: 'translateY(0px)' },
6+
'50%': { transform: 'translateY(-10px)' },
7+
});
8+
9+
const msgIn = keyframes({
10+
from: { opacity: 0, transform: 'translateY(8px)' },
11+
to: { opacity: 1, transform: 'translateY(0)' },
12+
});
13+
14+
const msgOut = keyframes({
15+
from: { opacity: 1, transform: 'translateY(0)' },
16+
to: { opacity: 0, transform: 'translateY(-6px)' },
17+
});
18+
19+
const cardIn = keyframes({
20+
from: { opacity: 0, transform: 'translateX(20px)' },
21+
to: { opacity: 1, transform: 'translateX(0)' },
22+
});
23+
24+
const cardOut = keyframes({
25+
from: { opacity: 1, transform: 'translateX(0)' },
26+
to: { opacity: 0, transform: 'translateX(-20px)' },
27+
});
28+
29+
const MOTION = '(prefers-reduced-motion: reduce)' as const;
30+
31+
export const container = style({
32+
display: 'flex',
33+
flexDirection: 'column',
34+
alignItems: 'center',
35+
justifyContent: 'center',
36+
minHeight: '60vh',
37+
padding: '40px 24px',
38+
});
39+
40+
export const mascotWrap = style({
41+
animation: `${float} 2.4s ease-in-out infinite`,
42+
marginBottom: '24px',
43+
'@media': { [MOTION]: { animation: 'none' } },
44+
});
45+
46+
export const mascotImg = style({
47+
width: '108px',
48+
height: 'auto',
49+
display: 'block',
50+
});
51+
52+
export const msgWrap = style({
53+
height: '24px',
54+
display: 'flex',
55+
alignItems: 'center',
56+
marginBottom: '16px',
57+
});
58+
59+
export const msg = style({
60+
fontSize: vars.typography.fontSize.sm,
61+
color: vars.color.neutral.n600,
62+
textAlign: 'center',
63+
fontWeight: vars.typography.fontWeight.medium,
64+
animation: `${msgIn} 0.35s ease-out both`,
65+
'@media': { [MOTION]: { animation: 'none' } },
66+
});
67+
68+
export const msgExiting = style({
69+
animation: `${msgOut} 0.25s ease-in both`,
70+
'@media': { [MOTION]: { animation: 'none' } },
71+
});
72+
73+
export const progressWrap = style({
74+
width: '280px',
75+
height: '4px',
76+
borderRadius: '2px',
77+
backgroundColor: vars.color.neutral.n200,
78+
overflow: 'hidden',
79+
marginBottom: '32px',
80+
});
81+
82+
export const progressFill = style({
83+
height: '100%',
84+
borderRadius: '2px',
85+
background: 'linear-gradient(90deg, #453EEF 0%, #9B86FF 100%)',
86+
width: '0%',
87+
willChange: 'width',
88+
});
89+
90+
export const tipCard = style({
91+
width: '280px',
92+
minHeight: '60px',
93+
backgroundColor: vars.color.neutral.n100,
94+
borderRadius: '12px',
95+
padding: '14px 16px',
96+
display: 'flex',
97+
alignItems: 'flex-start',
98+
gap: '10px',
99+
marginBottom: '10px',
100+
animation: `${cardIn} 0.4s ease-out both`,
101+
'@media': { [MOTION]: { animation: 'none' } },
102+
});
103+
104+
export const tipCardExiting = style({
105+
animation: `${cardOut} 0.3s ease-in both`,
106+
'@media': { [MOTION]: { animation: 'none' } },
107+
});
108+
109+
export const tipIcon = style({
110+
fontSize: '16px',
111+
flexShrink: 0,
112+
lineHeight: '22px',
113+
});
114+
115+
export const tipText = style({
116+
fontSize: vars.typography.fontSize.xs,
117+
color: vars.color.neutral.n600,
118+
lineHeight: vars.typography.lineHeight.lg,
119+
});
120+
121+
export const dots = style({
122+
display: 'flex',
123+
gap: '5px',
124+
marginBottom: '28px',
125+
});
126+
127+
export const dot = style({
128+
width: '5px',
129+
height: '5px',
130+
borderRadius: '50%',
131+
backgroundColor: vars.color.neutral.n300,
132+
transition: 'background-color 0.3s ease',
133+
});
134+
135+
export const dotActive = style({
136+
backgroundColor: vars.color.brand.primary,
137+
});
138+
139+
export const footer = style({
140+
display: 'flex',
141+
flexDirection: 'column',
142+
alignItems: 'center',
143+
gap: '10px',
144+
});
145+
146+
export const footerText = style({
147+
fontSize: vars.typography.fontSize.xs,
148+
color: vars.color.neutral.n400,
149+
});
150+
151+
export const cancelBtn = style({
152+
background: 'none',
153+
border: `1px solid ${vars.color.neutral.n300}`,
154+
borderRadius: vars.radius.sm,
155+
padding: '8px 20px',
156+
fontSize: vars.typography.fontSize.sm,
157+
color: vars.color.neutral.n500,
158+
cursor: 'pointer',
159+
fontFamily: vars.typography.fontFamily,
160+
transition: 'opacity 0.15s ease',
161+
selectors: {
162+
'&:hover': { opacity: 0.65 },
163+
},
164+
});

0 commit comments

Comments
 (0)