Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c2c87a7
feat: 이력 상세 화면에서 리스크 진단 결과를 DiagnosisDetailView로 표시
tmdcks1103 May 26, 2026
a5a6b1e
fix: 홈 대시보드 최근 분석 이력을 최신순 10개로 제한
tmdcks1103 May 26, 2026
14d31da
fix: 기능 없는 테이블 컬럼 정렬 화살표 제거
tmdcks1103 May 26, 2026
879a20d
fix: 기능 없는 요약 카드 화살표 제거
tmdcks1103 May 26, 2026
ed96756
fix: 엔진 미지원 공종도 Step4에 표시하고 선택 완료 안내 추가
tmdcks1103 May 26, 2026
93facd1
refactor: 가견적서 생성 사용자 입력 관련 리팩토링 (Step4 제거)
tmdcks1103 May 26, 2026
24c88ff
fix: 레이아웃에서 서버 유저를 클라이언트 인증 스토어에 동기화
tmdcks1103 May 26, 2026
7b4e488
fix: 가견적 결과 화면 참조 데이터 없는 공정 컬럼 숨김 및 버튼 정리
tmdcks1103 May 26, 2026
30a51d2
fix: 견적서 진단 결과 화면에서 기능 없는 PDF 다운로드 버튼 제거
tmdcks1103 May 26, 2026
d4a0bdf
feat: 분석 이력 페이지 목 데이터 제거 및 실제 API 연동
tmdcks1103 May 26, 2026
df4e83d
fix: 분석 이력 테이블 업체 컬럼 제거 및 리스크 표시 세분화
tmdcks1103 May 26, 2026
6b9b2c1
fix: AuthStoreInitializer에서 useEffect를 useLayoutEffect로 교체하여 auth 스토어…
tmdcks1103 May 26, 2026
d728ae6
fix: Next.js API 프록시 rewrites 추가
tmdcks1103 May 27, 2026
4eab0b3
fix: API 클라이언트 및 analyzeRisk 함수 수정
tmdcks1103 May 27, 2026
9b6d9ea
feat: 견적서 진단 리스크 집계 개선 및 오류 화면 다시 시도 버튼 추가
tmdcks1103 May 27, 2026
dfc99bc
refactor: 로딩 화면 공통 컴포넌트 추출
tmdcks1103 May 27, 2026
ec875e1
chore: main 브랜치와 병합 및 병합 중 충돌 해결
tmdcks1103 May 27, 2026
7786912
style : 로딩 화면 수정
tmdcks1103 May 27, 2026
c0dc13a
fix: 테스트 목 데이터 불일치 수정
tmdcks1103 May 27, 2026
3e472ef
fix: rewrites 제거
tmdcks1103 May 27, 2026
59d3f66
fix: beforeRetry 네트워크 오류 처리
tmdcks1103 May 28, 2026
9f8da39
fix: 고정 px 너비 반응형 처리
tmdcks1103 May 28, 2026
ec7bed1
fix: 접근성 누락 해결
tmdcks1103 May 28, 2026
173f826
fix: inferItemStatus 빈 title 미처리
tmdcks1103 May 28, 2026
64232db
fix: 부분 도배 미선택 시 통과
tmdcks1103 May 28, 2026
3da21c8
fix: summary.chips 직접 참조
tmdcks1103 May 28, 2026
47f3d66
fix: 불필요한 주석 삭제
tmdcks1103 May 28, 2026
2573882
refactor: LoadingScreen 반복·타이머 로직을 커스텀 훅으로 분리
tmdcks1103 May 28, 2026
c317d57
refactor: LoadingScreen UI를 역할별 하위 컴포넌트로 분리
tmdcks1103 May 28, 2026
6f86a43
refactor: LoadingScreen을 하위 컴포넌트 조립 레이어로 교체
tmdcks1103 May 28, 2026
564945f
refactor: Zustand authStore를 React Context(UserProvider)로 교체
tmdcks1103 May 28, 2026
2a4aff4
fix: 로딩 화면 위치 수정
tmdcks1103 May 28, 2026
81822e6
feat: 진단 요약을 카운트 배지에서 종합 인사이트 3문장으로 개선
tmdcks1103 May 28, 2026
294bd70
fix: useRotatingIndex에서 setInterval 콜백 내 setTimeout(tid) 미정리 버그 수정
tmdcks1103 May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/web/src/api/analyze/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { type RiskDetectAnalyzeResponse, type RiskSaveBody } from './types';

const isDev = process.env.NODE_ENV === 'development';

export const analyzeRisk = async (formData: FormData, signal?: AbortSignal): Promise<RiskDetectAnalyzeResponse> => {
export const analyzeRisk = async (
formData: FormData,
userId: number,
signal?: AbortSignal
): Promise<RiskDetectAnalyzeResponse> => {
if (isDev) {
console.group('[진단] analyzeRisk — POST /api/v1/risk-detector/analyze');
for (const [key, value] of formData.entries()) {
Expand All @@ -18,7 +22,7 @@ export const analyzeRisk = async (formData: FormData, signal?: AbortSignal): Pro
const res = await client
.post('api/v1/risk-detector/analyze', {
body: formData,
headers: { 'content-type': undefined },
headers: { 'content-type': undefined, 'x-user-id': String(userId) },
timeout: 180000,
signal,
})
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ const createClient = () =>
hooks: {
beforeRetry: [
async ({ request, error }) => {
const status = (error as { status?: number }).status;
if (status !== undefined && status !== 401) {
throw error;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (isPreAuth(request.url) || isRefresh(request.url)) {
throw error;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import { useLayoutEffect } from 'react';
import { useAuthStore } from '@/store/authStore';
import { type AuthUser } from '@/types/auth';

interface AuthStoreInitializerProps {
user: AuthUser;
}

export const AuthStoreInitializer = ({ user }: AuthStoreInitializerProps) => {
const setUser = useAuthStore((s) => s.setUser);

useLayoutEffect(() => {
setUser(user);
}, [user, setUser]);

return null;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 사용자 인증을 서버에서 직접 하고 있는데 zustand로 인증을 관리하는 이유가 있나요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UploadPanel, Step5EstimateReview에서 API 호출 시 user.id가 필요한데,
서버 전용 함수를 직접 호출할 수 없어서 AuthStoreInitializer가 user를 Zustand에 주입해주는 구조입니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인증한정으로 user.id를 사용하는 거면 React Context (UserProvider) 로도 동일하게 server → client로 user 데이터를 흘려보낼 수 있고, 이쪽이 더 단순할 것 같습니다!!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 굳이 zudstand을 사용할 필요는 없었던 것 같네요! 반영하겠습니다🫡

259 changes: 259 additions & 0 deletions apps/web/src/app/(main)/_components/LoadingScreen/LoadingScreen.css.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 주석은 삭제해주세요!!

Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { vars } from '@muneo/design-system';
import { keyframes, style } from '@vanilla-extract/css';

const float = keyframes({
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-12px)' },
});

const shadowPulse = keyframes({
'0%, 100%': { transform: 'scaleX(1)', opacity: 0.18 },
'50%': { transform: 'scaleX(0.72)', opacity: 0.08 },
});

const workingBounce = keyframes({
'0%, 80%, 100%': { transform: 'translateY(0)', opacity: 0.35 },
'40%': { transform: 'translateY(-6px)', opacity: 1 },
});

const msgIn = keyframes({
from: { opacity: 0, transform: 'translateY(8px)' },
to: { opacity: 1, transform: 'translateY(0)' },
});

const msgOut = keyframes({
from: { opacity: 1, transform: 'translateY(0)' },
to: { opacity: 0, transform: 'translateY(-6px)' },
});

const shimmer = keyframes({
from: { transform: 'translateX(-100%)' },
to: { transform: 'translateX(400%)' },
});

const cardIn = keyframes({
from: { opacity: 0, transform: 'translateX(24px)' },
to: { opacity: 1, transform: 'translateX(0)' },
});

const cardOut = keyframes({
from: { opacity: 1, transform: 'translateX(0)' },
to: { opacity: 0, transform: 'translateX(-24px)' },
});

const MOTION = '(prefers-reduced-motion: reduce)' as const;

export const container = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '60vh',
padding: '40px 24px',
});

/* ── 마스코트 ── */
export const mascotWrap = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginBottom: '8px',
});

export const mascotFloating = style({
animation: `${float} 2.6s ease-in-out infinite`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const mascotImg = style({
width: '92px',
height: 'auto',
display: 'block',
});

export const mascotShadow = style({
width: '56px',
height: '8px',
borderRadius: '50%',
background: 'rgba(132, 85, 223, 0.22)',
filter: 'blur(4px)',
marginTop: '2px',
animation: `${shadowPulse} 2.6s ease-in-out infinite`,
'@media': { [MOTION]: { animation: 'none' } },
});

/* ── Working dots ── */
export const workingDots = style({
display: 'flex',
gap: '6px',
justifyContent: 'center',
marginTop: '12px',
marginBottom: '20px',
});

export const workingDot = style({
width: '7px',
height: '7px',
borderRadius: '50%',
backgroundColor: vars.color.brand.primary,
opacity: 0.35,
animation: `${workingBounce} 1.4s ease-in-out infinite`,
'@media': { [MOTION]: { animation: 'none', opacity: 1 } },
});

/* ── 상태 메시지 ── */
export const msgWrap = style({
height: '22px',
display: 'flex',
alignItems: 'center',
marginBottom: '14px',
});

export const msg = style({
fontSize: vars.typography.fontSize.sm,
color: vars.color.neutral.n500,
textAlign: 'center',
fontWeight: vars.typography.fontWeight.medium,
animation: `${msgIn} 0.35s ease-out both`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const msgExiting = style({
animation: `${msgOut} 0.25s ease-in both`,
'@media': { [MOTION]: { animation: 'none' } },
});

/* ── 프로그레스 바 ── */
export const progressWrap = style({
width: '320px',
height: '5px',
borderRadius: '10px',
backgroundColor: vars.color.neutral.n200,
overflow: 'hidden',
marginBottom: '32px',
position: 'relative',
});

export const progressFill = style({
height: '100%',
borderRadius: '10px',
background: 'linear-gradient(90deg, #7B5CE5 0%, #B199FF 100%)',
width: '0%',
willChange: 'width',
position: 'relative',
overflow: 'hidden',
selectors: {
'&::after': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '25%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent)',
animation: `${shimmer} 2.2s ease-in-out infinite`,
},
},
'@media': {
[MOTION]: {
selectors: {
'&::after': { animation: 'none' },
},
},
},
});

/* ── 팁 카드 ── */
export const tipCard = style({
width: '480px',
backgroundColor: '#F5F0FF',
border: '1px solid #E3D8FF',
borderRadius: '16px',
padding: '20px 24px 16px',
display: 'flex',
flexDirection: 'column',
gap: '10px',
marginBottom: '20px',
animation: `${cardIn} 0.4s cubic-bezier(0.22, 1, 0.36, 1) both`,
'@media': { [MOTION]: { animation: 'none' } },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

export const tipCardExiting = style({
animation: `${cardOut} 0.28s ease-in both`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const tipHeader = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
});

export const tipLabel = style({
display: 'flex',
alignItems: 'center',
gap: '5px',
fontSize: vars.typography.fontSize.xs,
fontWeight: vars.typography.fontWeight.semiBold,
color: '#7B5CE5',
letterSpacing: '0.02em',
});

export const tipCounter = style({
fontSize: vars.typography.fontSize.xs,
color: '#B199FF',
fontWeight: vars.typography.fontWeight.medium,
fontVariantNumeric: 'tabular-nums',
});

export const tipText = style({
fontSize: vars.typography.fontSize.base,
color: vars.color.neutral.n700,
lineHeight: '1.75',
fontWeight: vars.typography.fontWeight.regular,
wordBreak: 'keep-all',
});

export const tipProgressTrack = style({
width: '100%',
height: '3px',
borderRadius: '10px',
backgroundColor: '#E3D8FF',
overflow: 'hidden',
marginTop: '4px',
});

export const tipProgressFill = style({
height: '100%',
borderRadius: '10px',
background: 'linear-gradient(90deg, #8455DF, #B199FF)',
transition: 'width 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
});

/* ── 푸터 ── */
export const footer = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
});

export const footerText = style({
fontSize: vars.typography.fontSize.xs,
color: vars.color.neutral.n400,
});

export const cancelBtn = style({
background: 'none',
border: `1px solid ${vars.color.neutral.n300}`,
borderRadius: vars.radius.sm,
padding: '8px 20px',
fontSize: vars.typography.fontSize.sm,
color: vars.color.neutral.n500,
cursor: 'pointer',
fontFamily: vars.typography.fontFamily,
transition: 'opacity 0.15s ease',
selectors: {
'&:hover': { opacity: 0.65 },
},
});
Loading
Loading