Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c5b03f
feat: 위협 유형별 상세 설명 매핑
kim-subsub May 14, 2026
b8dd177
feat: 평판 섹션 지표 개편
kim-subsub May 14, 2026
bd1cafa
feat: 테스트 QR 코드 생성 명령 추가
kim-subsub May 14, 2026
9168b34
fix: 외부 링크 검증 및 보안 감사 통과
kim-subsub May 14, 2026
c570fcf
feat: 비 URL 스키마 미리보기 제거
kim-subsub May 14, 2026
be7b014
fix: 결과 화면 점수 기반 위험도 반영
kim-subsub May 14, 2026
f854324
fix: 스캔 이력 점수 기반 위험도 반영
kim-subsub May 14, 2026
db42bed
fix: 비 URL 결과 라벨 타입 오류 수정
kim-subsub May 14, 2026
c24e586
chore: 타입 체크 CI 단계 추가
kim-subsub May 14, 2026
e856ff4
docs: 리팩토링 로드맵 정리
kim-subsub May 14, 2026
d58b082
fix: 비 URL 실행 스킴 보안 강화
kim-subsub May 14, 2026
a608d03
docs: 리팩토링 진행 기록 갱신
kim-subsub May 14, 2026
2088b14
refactor: 스캔 응답 정규화 유틸 분리
kim-subsub May 14, 2026
ab6c140
docs: 응답 정규화 진행 기록 갱신
kim-subsub May 14, 2026
997ee96
refactor: 스캔 URL 해석 공통화
kim-subsub May 14, 2026
7b8161e
docs: URL 해석 공통화 기록 갱신
kim-subsub May 14, 2026
586c24f
refactor: 스캔 세션 상태 전환 분리
kim-subsub May 14, 2026
ad9554f
docs: 상태 관리 리팩토링 기록 갱신
kim-subsub May 14, 2026
d8a4ab1
refactor: 로딩 결과 전환 로직 분리
kim-subsub May 14, 2026
bbce837
docs: 로딩 SSE 분리 기록 갱신
kim-subsub May 14, 2026
91d32d2
refactor: 로딩 상세 조회 정책 분리
kim-subsub May 14, 2026
68329a1
docs: 로딩 상세 조회 분리 기록 갱신
kim-subsub May 14, 2026
87235eb
refactor: 리포트 데이터 섹션 빌더 분리
kim-subsub May 14, 2026
f685136
docs: 리포트 데이터 분리 기록 갱신
kim-subsub May 14, 2026
20d94e7
refactor: 리포트 도메인 서버 빌더 분리
kim-subsub May 14, 2026
4ebd620
docs: 리팩토링 완료 문서 최신화
kim-subsub May 14, 2026
4b79030
fix: 리뷰 코멘트 반영
kim-subsub May 14, 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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ jobs:
- name: Run tests
run: pnpm run test

- name: Run TypeScript typecheck
run: pnpm run typecheck

- name: Build application
run: pnpm run build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ out
docs/local/
.claude/
.devserver.*.log
src/test-code/generated-qr/
1 change: 1 addition & 0 deletions .secretlintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ build
out
.git
package-lock.json
src/test-code/generated-qr/
147 changes: 126 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,162 @@
# Veri-Q
# Veri-Q Client

의심스러운 QR 코드와 링크를 빠르게 점검하고, 결과에 따라 대응 가이드를 제공하는 퀴싱 방지 웹사이트입니다.
Veri-Q는 QR 코드 이미지에서 URL 또는 비 URL 스킴을 분석하고, 위험도에 맞는 결과 페이지와 상세 리포트를 제공하는 React 클라이언트입니다.

## Tech Stack

| 분야 | 기술 |
| ------------- | ---------------------------------- |
| Language | TypeScript |
| Frontend | React 19 |
| Build Tool | Vite |
| Routing | TanStack Router |
| Styling | vanilla-extract (theme tokens) |
| Quality | ESLint, Prettier, Vitest |
| CI / Security | GitHub Actions, CodeQL, Secretlint |
| Area | Stack |
| -------- | ------------------------------------ |
| Language | TypeScript |
| Frontend | React 19 |
| Build | Vite |
| Routing | TanStack Router |
| State | Zustand |
| Styling | vanilla-extract |
| UI | Ant Design |
| Quality | ESLint, Prettier, Vitest, TypeScript |
| Security | Secretlint, pnpm audit, CodeQL |

## Project Structure
## Architecture

현재 구조는 FSD를 가볍게 적용한 FSD-lite 방식입니다.

```text
src/
pages/ # page-level ui
routes/ # tanstack router setup
features/ # user action features
shared/ # shared ui, libs, types, constants
pages/ # 페이지 조합, page-local hooks/lib/ui
routes/ # TanStack Router 설정과 route-level lazy loading
features/ # 사용자 액션 중심 기능 API와 타입
shared/ # 공통 API, store, lib, UI, icon, type
test-code/ # 테스트용 QR PNG 생성 스크립트
```

## Getting Started
주요 정리 내용:

- `shared/lib/scan-session`: URL/스키마/위험도/결과 라우트/스캔 식별자 공통화
- `shared/store`: 스캔 세션 상태 전환을 순수 함수로 분리
- `pages/Loading/lib`: SSE 완료 판정, 상세 조회 retry, polling 정책 분리
- `pages/Report/lib`: 리포트 컨텍스트, 평판, 도메인, 서버 정보 섹션 빌더 분리
- `routes/router.tsx`: 페이지 단위 `lazy()`와 `Suspense` 적용
- `vite.config.ts`: `react`, `antd`, `router`, `state` vendor chunk 분리

## Setup

```bash
pnpm install
```

설치 중 `postinstall` 스크립트가 실행되며 예시 파일을 기준으로 `.env.local`과 `.env.server.local`을 생성합니다.

필요한 환경 파일:

- `.env.local`: 브라우저에서 사용하는 Vite 환경 변수
- `.env.server.local`: 로컬 captcha verify 서버의 서버 전용 secret

예시 파일:

- [.env.example](./.env.example)
- [.env.server.example](./.env.server.example)

## Development

```bash
pnpm dev
```

기본 개발 서버 주소:
기본 주소:

```text
http://localhost:5173
```

로컬 개발 서버는 Vite 앱과 captcha verify 서버를 함께 실행합니다.

프론트만 실행하려면:

```bash
pnpm dev:web
```

## Backend Proxy

로컬 Vite proxy와 Vercel rewrite는 다음 경로를 사용합니다.

| Path | Target |
| -------- | ------------- |
| `/be1/*` | BE1 API |
| `/be3/*` | BE3 API / SSE |

주요 환경 변수:

| Variable | Description |
| ------------------------ | ---------------------------------------- |
| `VITE_BE1_BASE_URL` | 브라우저 기준 BE1 base URL 또는 `/be1` |
| `VITE_BE3_BASE_URL` | 브라우저 기준 BE3 base URL 또는 `/be3` |
| `BE1_PROXY_TARGET_URL` | Vercel Function에서 사용할 BE1 실제 대상 |
| `BE3_PROXY_TARGET_URL` | Vercel Function에서 사용할 BE3 실제 대상 |
| `VITE_API_TIMEOUT_MS` | 일반 API timeout |
| `VITE_UPLOAD_TIMEOUT_MS` | QR 이미지 업로드 timeout |
| `VITE_SSE_RECONNECT_MAX` | SSE 최대 재연결 횟수 |

## Scripts

```bash
pnpm dev
pnpm dev:web
pnpm build
pnpm start
pnpm preview
pnpm lint
pnpm lint:fix
pnpm format
pnpm format:write
pnpm test
pnpm typecheck
pnpm security:check
```

CI 기준으로 로컬에서 확인할 때는 아래 명령을 모두 통과시키면 됩니다.

```bash
pnpm format
pnpm lint
pnpm test
pnpm typecheck
pnpm security:check
pnpm build
```

## Test QR Code

테스트 URL을 QR PNG로 만들 수 있습니다.

```bash
pnpm test-code https://naver.com
```

`//`가 빠진 입력도 보정합니다.

```bash
pnpm test-code: https:naver.com
```

생성 위치:

```text
src/test-code/generated-qr/
```

PowerShell에서 쿼리스트링에 `&`가 있는 URL은 따옴표로 감싸세요.

```bash
pnpm test-code "https://example.com/?a=1&b=2"
```

## Commit Safety

- `.env.local`과 `.env.server.local`은 커밋하지 않습니다.
- pre-commit hook에서 secretlint, eslint, prettier가 staged file 기준으로 실행됩니다.
- 보안 확인은 `pnpm security:check`로 수행합니다.

## Documents

- [협업 가이드](./CONTRIBUTING.md)
- [코드 컨벤션](./docs/convention.md)
- [작업 컨벤션](./docs/convention.md)
- [리팩토링 로드맵](./docs/refactor-roadmap.md)
137 changes: 85 additions & 52 deletions docs/convention.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
# Convention

> 이 문서는 프로젝트의 기본 코드 컨벤션을 정리한 문서입니다. 설명보다 규칙과 예시를 우선합니다.
이 문서는 Veri-Q Client의 기본 코드 작성 규칙입니다. 설명보다 일관성을 우선하고, 기존 구조와 가까운 방식으로 변경합니다.

## 기본 원칙
## Architecture

- 일관성을 우선합니다.
- 한 파일은 한 가지 책임만 갖도록 작성합니다.
- 페이지는 조합에 집중하고, 사용자 액션 로직은 `features`에 둡니다.
- 불필요한 레이어를 만들지 않습니다. 필요할 때만 추가합니다.

## 폴더 구조
현재 프로젝트는 FSD-lite 구조를 사용합니다.

```text
src/
pages/ # 페이지 UI 컴포넌트
routes/ # TanStack Router 설정 및 라우트 연결
pages/ # 페이지 조합, page-local hooks/lib/ui
routes/ # TanStack Router 설정
features/ # 사용자 액션 단위 기능
shared/ # 공통 UI, 유틸, 타입, 상수, API
widgets/ # (선택) 여러 요소를 조합한 큰 UI 블록
shared/ # 공통 API, store, lib, UI, icon, type
test-code/ # 테스트 보조 스크립트
```

## 컴포넌트 선언 및 export
원칙:

- `pages`는 화면 조합과 페이지 전용 로직을 둔다.
- `features`는 업로드, 이력 조회처럼 사용자 액션 중심 기능을 둔다.
- `shared`는 여러 영역에서 재사용하는 API, store, lib, type, UI만 둔다.
- `widgets`는 아직 사용하지 않는다. 여러 페이지에서 재사용되는 조합 UI가 생길 때 추가한다.
- 새 레이어는 필요가 명확할 때만 만든다.

- 페이지/컴포넌트 파일은 `default export`를 사용합니다.
- 유틸/훅/상수는 `named export`를 사용합니다.
- 익명 `default export`는 사용하지 않습니다.
## Exports

짧은 예시:
- 페이지 컴포넌트는 `default export`를 사용한다.
- 유틸, 상수, 타입, store는 `named export`를 사용한다.
- 익명 `default export`는 사용하지 않는다.

```tsx
export default function HomePage() {
export default function ReportPage() {
return <main />;
}
```
Expand All @@ -38,58 +39,90 @@ export default function HomePage() {
export const MAX_URL_LENGTH = 2048;
```

## import 순서
## Import Order

1. 프레임워크/런타임 모듈
ESLint `import/order` 규칙을 따른다.

1. Node/builtin
2. 외부 패키지
3. 프로젝트 내부 절대 경로
4. 현재 디렉터리 기준 상대 경로
3. 내부 alias 경로
4. 상대 경로
5. type import

프로젝트 내부 절대 경로 순서:
내부 alias 우선순위:

`shared -> features -> widgets -> pages -> routes -> local`
```text
shared -> features -> widgets -> pages -> routes
```

추가 규칙:
규칙:

- 그룹 사이에는 한 줄 공백을 둡니다.
- 타입 전용 import는 `import type`으로 분리합니다.
- import 그룹 사이에는 빈 줄을 둔다.
- 같은 그룹 안에는 불필요한 빈 줄을 두지 않는다.
- 타입 전용 import는 `import type`을 사용한다.

## 파일명 규칙
## File Naming

| 구분 | 규칙 | 예시 |
| --------------------- | --------------------- | --------------------------------- |
| 폴더명 | `kebab-case` | `scan-url`, `result-summary` |
| 컴포넌트 파일 | `PascalCase` | `HomePage.tsx`, `UrlScanForm.tsx` |
| util 함수 / 상수 파일 | `lowerCamelCase` | `formatDate.ts`, `constants.ts` |
| 테스트 파일 | 대상 파일명 + `.test` | `isSafeExternalUrl.test.ts` |
| Target | Rule | Example |
| ----------------- | --------------------- | ---------------------------- |
| Folder | `kebab-case` | `scan-url`, `result-summary` |
| Page/component | `PascalCase` | `ReportPage.tsx` |
| Hook | `useSomething.ts` | `useLoadingPage.ts` |
| Utility/constants | `lowerCamelCase` | `scanResultRoute.ts` |
| Test | source name + `.test` | `scanResultRoute.test.ts` |

REST API 관련 파일명/함수명 접두사:
API 함수 접두어:

- GET: `fetch`
- POST: `submit`
- DELETE: `remove`
- PUT/PATCH: `update`
- DELETE: `remove`

## 슬라이스 공개 범위
## Page Logic

- 슬라이스 외부 공개 진입점은 가능하면 `index.ts`로 관리합니다.
- 다른 슬라이스의 내부 경로 직접 import는 지양합니다.
- 페이지 컴포넌트는 화면 조합에 집중한다.
- 복잡한 데이터 변환은 `lib`의 순수 함수로 분리한다.
- 페이지 전용 hook은 `hooks`에 둔다.
- 여러 페이지에서 쓰는 로직은 `shared/lib` 또는 `shared/store`로 올린다.
- 테스트 가능한 로직은 UI에서 빼서 먼저 테스트할 수 있게 만든다.

## 컴포넌트 작성 규칙
## State

- 페이지 컴포넌트는 화면 조합과 데이터 연결에 집중합니다.
- `shared/ui`에는 도메인 의존성이 없는 공용 UI만 둡니다.
- 재사용되지 않는 컴포넌트는 해당 기능/페이지 근처에 둡니다.
- `utils.ts`, `helpers.ts`, `common.ts`처럼 의미가 약한 파일명은 지양합니다.
- Zustand store는 상태 보관과 액션 노출에 집중한다.
- 상태 전환 계산은 가능한 순수 함수로 분리한다.
- persist 대상은 필요한 최소 필드만 저장한다.

## Security

- 외부 링크와 실행 스킴은 허용 목록 기반으로 처리한다.
- 위험하거나 사용자 행동을 유발하는 스킴은 확인 단계를 둔다.
- `.env.local`, `.env.server.local`은 커밋하지 않는다.
- 새 API/보안 정책 변경은 성공 케이스와 차단 케이스를 모두 테스트한다.

## Tests

- 공통 유틸과 데이터 변환은 단위 테스트를 붙인다.
- 큰 리팩토링 전에는 기존 회귀 테스트를 보강한다.
- CI 기준 검증은 다음 명령이다.

```bash
pnpm format
pnpm lint
pnpm test
pnpm typecheck
pnpm security:check
pnpm build
```

## 접근성
## Accessibility

- `header`, `nav`, `main`, `section`, `article`, `aside`, `footer` 같은 시맨틱 태그를 우선 사용합니다.
- 인터랙티브 요소에는 텍스트, `label`, `aria-label` 중 하나가 필요합니다.
- 아이콘만 있는 버튼은 반드시 `aria-label`을 추가합니다.
- `main`, `section`, `article`, `nav`, `header`, `footer` 같은 시맨틱 태그를 우선한다.
- 인터랙티브 요소에는 텍스트 label 또는 `aria-label`을 제공한다.
- 아이콘만 있는 버튼에는 반드시 `aria-label`을 둔다.

## Router Note
## Routing

- 라우트 정의는 `src/routes`에 둡니다.
- 화면 컴포넌트는 `src/pages`에서 import 합니다.
- 파일 기반 자동 생성 대신 명시적인 라우트 트리 구성을 기본으로 사용합니다.
- 라우트 정의는 `src/routes`에 둔다.
- 페이지 컴포넌트는 `src/pages`에서 import한다.
- 페이지는 route-level `lazy()`로 불러온다.
- 라우트 fallback은 접근 가능한 `role="status"` 영역으로 제공한다.
Loading
Loading