first commit

This commit is contained in:
2026-09-04 00:33:52 +09:00
commit 8b72e06784
238 changed files with 44441 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
---
name: checklist
description: 신규 프로젝트 셋업 절차 + 매 작업 공통 점검(에러코드 SSOT 동기화 등). 특정 시점의 구현 상태가 아니라 언제나 유효한 공용 규칙만 담는다.
---
# 작업 점검 & 신규 프로젝트 셋업
> 이 저장소는 **재사용 템플릿**이다. 클론해 어떤 프로젝트든 시작할 수 있도록, 여기에는 **언제나 유효한 공용 규칙**만 둔다(특정 시점의 구현 상태는 두지 않는다).
> 각 스택이 기본 제공하는 골격과 확장 규칙은 [[nestjs]] · [[vue3]] · [[flutter]] · [[docker]] 스킬 참조.
---
## 1. 신규 프로젝트 셋업 (클론 직후 1회)
- [ ] 루트 `.env.{env}.example``.env.{env}` 복사 (루트가 설정 SSOT — 대표 키가 채워져 있음, [[env-structure]])
- [ ] `flutter/.env.example``flutter/.env` 복사 후 `API_URL` 확인 (Android 에뮬레이터: `10.0.2.2`)
- [ ] 프로젝트 식별 값 교체 (`PROJECT_NAME` 등 루트 env 키)
- [ ] 백엔드 고유 시크릿(`JWT_SECRET` 등)을 `backend/.env.{env}` 에 작성 (compose `env_file` 로 로드)
- [ ] Docker 개발 환경 기동으로 동작 확인 ([[docker]])
## 2. 커밋 전 공통 점검 (매 작업)
- [ ] 주석은 한국어, 매직 넘버·하드코딩 문자열 없음 ([[coding-common]])
- [ ] 린트/분석 통과 — backend `npm run lint`, frontend `npm run lint && npm run type-check`, flutter `flutter analyze` (Warning 0)
- [ ] 절대경로 alias 사용 (TS `@/`, Dart `package:`), 깊은 상대경로 없음
- [ ] 민감정보(`.env`, 시크릿) 미커밋 — `*.example` 만 추적 ([[security]])
- [ ] 커밋 메시지 `<type>: <한국어 제목>` 형식, 한 커밋 한 변경 ([[git-convention]])
## 3. 에러 코드 SSOT 동기화 (메시지/코드 변경 시 반드시 3곳 동시 수정)
- [ ] backend `src/common/filters/http-exception.filter.ts`
- [ ] frontend `src/composables/useApi.ts` (`ErrorCodeLexicon`)
- [ ] flutter `lib/core/api/error_lexicon.dart` (`ErrorLexicon`)
> `AUTH_002`(세션 만료)는 **클라이언트 전용** 코드로 서버 응답에는 나타나지 않는다. 자세한 내용은 [[error-codes]].
---
## 4. 기능 확장 시 따르는 순서
새 기능은 만들기 전에, 해당 스킬의 **"템플릿 기본 제공 골격"** 에서 재사용할 패턴을 먼저 찾는다(같은 역할을 새로 만들지 말 것).
1. 해당 디렉터리 스킬 규칙 확인 ([[nestjs]] / [[vue3]] / [[flutter]])
2. 기본 제공 골격(인터셉터·필터·dio client·error lexicon·예제 모듈)에서 패턴 재사용
3. 표준 응답/에러 흐름 준수 ([[response-format]] · [[error-codes]])
4. env 신규 값은 루트 `.env` 부터 ([[env-structure]])
5. 커밋 전 위 2·3절 점검 통과
> 대표 확장 지점(각 스킬의 "확장 시 따르는 규칙" 참조):
> - 인증/권한 → [[nestjs]](guards·JWT) · [[vue3]](auth store + 라우터 가드)
> - 데이터 영속화 → [[nestjs]](TypeORM 엔티티/리포지토리)
> - 상태·화면 계층 → [[flutter]](Riverpod 프로바이더 · features 구조)
+27
View File
@@ -0,0 +1,27 @@
---
name: coding-common
description: 세 클라이언트(backend/frontend/flutter) 공통 코딩 규칙 — 주석 언어, 타입 정책, 린트, 순수 유틸 분리, 절대경로 alias.
---
# 공통 코딩 규칙
- 모든 코드 주석은 **한국어**로 작성
- 웹(`frontend`)·서버(`backend`)는 100% TypeScript, 모바일(`flutter`)은 Dart Sound Null Safety 준수
- 컴파일러/린터 **Warning 방치 금지** — 커밋 전 각 프로젝트 `lint` / `analyze` 통과
- 유틸 함수는 순수 함수로 분리 — TS는 `shared/utils/`, Dart는 `core/utils/`
- 매직 넘버·하드코딩 문자열 지양 → 상수 또는 환경변수로 분리
- 절대경로 alias 사용 (TS `@/`, Dart `package:`), 깊은 상대경로(`../../`) 지양
## 네이밍 규약
| 대상 | 규칙 | 예 |
| --- | --- | --- |
| TS 파일(일반) | kebab-case | `user.store.ts`, `use-api.ts` / 기존 composable 은 `useApi.ts` 형태 유지 |
| Vue 컴포넌트 파일 | PascalCase | `BaseButton.vue`, `UserCard.vue` |
| Dart 파일·디렉터리 | snake_case | `dio_client.dart`, `user_repository.dart` |
| 클래스/타입 | PascalCase | `UserModel`, `TransformInterceptor` |
| 변수/함수 | camelCase (Dart 포함) | `getUser`, `isLoggedIn` |
| 상수 | TS `UPPER_SNAKE` 또는 모듈 상수, Dart `lowerCamel` | `MAX_RETRY`, `defaultTimeout` |
| 에러 코드 | `DOMAIN_NNN` | `AUTH_001`, `VAL_001` ([[error-codes]]) |
> 도메인별 세부 규칙: [[nestjs]] · [[vue3]] · [[flutter]] · 테스트: [[testing]]
+30
View File
@@ -0,0 +1,30 @@
---
name: env-structure
description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{APP_ENV} 가 모든 설정의 SSOT, docker-compose 가 서비스에 주입. 새 설정값 추가/서비스 env 작성 규칙.
---
# 환경변수 구조 (단일 진실 공급원: 루트 `.env`)
이 템플릿은 **루트 `.env.{APP_ENV}` 가 모든 설정의 단일 진실 공급원(SSOT)** 이다.
값을 한 곳에서만 관리하기 위해, 서비스(backend/frontend)는 자체 env에 값을 중복 작성하지 않고 **루트 → docker-compose 가 주입한 값을 그대로 사용**한다.
```
루트 .env.{APP_ENV}
│ (docker compose --env-file 로 로드)
├─▶ backend : env_file + environment 로 DB/Redis/CORS/포트 주입
│ (backend/.env.{APP_ENV} 는 "존재"만 하면 됨, 내용 최소)
├─▶ frontend : BACKEND_API_URL → build arg(VITE_API_BASE_URL) 로 주입
├─▶ db : DB_USER/DB_PASSWORD/DB_NAME, DB_DATA_DIR 볼륨
└─▶ redis : REDIS_PASSWORD, REDIS_DATA_DIR 볼륨
```
규칙:
- **새 설정값은 루트 `.env.{APP_ENV}` 에 먼저 추가**하고, 필요한 서비스에 `docker-compose.yml``environment` / `args` 로 전달한다.
- 서비스 자체 env 파일에는 **루트에서 내려주지 않는 고유 값만** 작성한다. (예: 백엔드 `JWT_SECRET`)
- `docker-compose.yml``env_file` 지시자 때문에 `backend/.env.{APP_ENV}` 파일은 비어 있어도 **존재해야 한다.**
- **Flutter 는 예외** — docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립적으로 관리한다.
- 모든 env 파일은 커밋 금지, `*.example` 만 커밋한다. (`.gitignore``!*.example` 재포함 규칙)
> 루트 env의 대표 키: `PROJECT_NAME`, `APP_ENV`, `BUILD_MODE`, 각종 `*_PORT`, `BACKEND_API_URL`, `CORS_ORIGIN`, `DB_*`/`DB_DATA_DIR`, `REDIS_*`/`REDIS_DATA_DIR`. 실제 목록은 `.env.development.example` 참조.
> 실행/배포 명령은 [[docker]], 보안 규칙은 [[security]] 참조.
+29
View File
@@ -0,0 +1,29 @@
---
name: error-codes
description: 세 플랫폼이 공유하는 에러 코드 ↔ 한국어 메시지 매핑(SSOT). 메시지 수정 시 backend/frontend/flutter 3개 파일을 함께 갱신한다.
---
# 에러 코드 맵핑
세 플랫폼이 공유하는 **단일 진실 공급원(SSOT)**. 메시지를 수정할 때는 아래 세 파일을 함께 갱신한다.
- backend: `src/common/filters/http-exception.filter.ts`
- frontend: `src/composables/useApi.ts` (`ErrorCodeLexicon`)
- flutter: `lib/core/api/error_lexicon.dart` (`ErrorLexicon`)
| 코드 | HTTP / 상황 | 메시지 |
| -------- | -------------------- | ------------------------------------------------------- |
| SYS_001 | 500 / 미정의 예외 | "일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." |
| AUTH_001 | 401 | "로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요." |
| AUTH_002 | 세션 만료 (클라이언트) | "안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요." |
| AUTH_003 | 403 | "해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다." |
| VAL_001 | 400 / 422 (Validation) | "입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)" |
| RES_001 | 404 | "요청하신 정보나 페이지를 찾을 수 없습니다." |
| BIZ_001 | 200(비즈니스 실패) / 기타 | "요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요." |
> 비즈니스 예외는 `HttpException` 응답의 `message` 로 사용자 노출 문구를 재정의할 수 있다.
> 응답 래퍼 구조는 [[response-format]] 참조.
> **구현 주의:** 백엔드 필터(`http-exception.filter.ts`)는 **6개 코드**(SYS_001 / AUTH_001 / AUTH_003 / VAL_001 / RES_001 / BIZ_001)만 방출한다.
> `AUTH_002`(세션 만료)는 **클라이언트(frontend/flutter)에서만** 생성하는 코드로, 서버 응답에는 나타나지 않는다.
> 코드/메시지 변경 시 위 3개 파일을 함께 갱신해야 하며, 점검 항목은 [[checklist]] 참조.
+12
View File
@@ -0,0 +1,12 @@
---
name: git-convention
description: Git 커밋 메시지 컨벤션 — <type>: <제목> 형식, 한국어 명령형, 한 커밋 한 논리 변경.
---
# Git 커밋 컨벤션
`<type>: <제목>` 형식 — type: **feat / fix / refactor / docs / chore / perf / test / style**
- 예: `feat: 사용자 목록 페이지네이션 추가`
- 제목은 한국어, 명령형 현재 시제로 작성
- 한 커밋은 하나의 논리적 변경만 포함
+21
View File
@@ -0,0 +1,21 @@
---
name: response-format
description: 모든 API 응답이 따르는 표준 응답 래퍼(success/data/error) 규칙. SSOT는 backend TransformInterceptor + HttpExceptionFilter.
---
# 표준 응답 포맷 (Global Response Wrapper)
모든 API 응답은 아래 구조로 통일한다.
(SSOT: backend `TransformInterceptor` + `HttpExceptionFilter` — [[nestjs]] 참조)
```jsonc
// 성공
{ "success": true, "data": { /* ... */ } }
// 실패
{ "success": false, "error": { "code": "VAL_001", "message": "..." } }
```
- 프론트/플러터 인터셉터는 `success: true``data` 만 언래핑하여 반환한다.
- `success: false` 이거나 HTTP 4xx/5xx 면 `error.code` 로 메시지를 매핑한다.
- 에러 코드 ↔ 메시지 매핑은 [[error-codes]] 참조.
+15
View File
@@ -0,0 +1,15 @@
---
name: security
description: 프로젝트 공통 보안 규칙 — 민감정보 커밋 금지, JWT 쿠키/secure storage, SQL Injection 차단, DTO 검증, 보안 헤더/CORS/Rate Limiting, 컨테이너 비-root 구동.
---
# 보안
- `.env*` 등 민감정보 Git 커밋 금지 (`.gitignore` 확인) — 예시는 `*.example`(예: `.env.development.example`, `.env.example`) 로만 공유
- JWT는 HttpOnly·Secure 쿠키 사용 (웹 `localStorage` 금지) / 모바일은 `flutter_secure_storage` 사용
- TypeORM ORM 쿼리 사용으로 SQL Injection 차단 (Raw 쿼리 시 파라미터 바인딩 필수)
- 입력값은 DTO(`class-validator`) 검증을 거친 뒤 서비스 계층에 전달
- 보안 헤더(`helmet`)·CORS·Rate Limiting(`@nestjs/throttler`) 적용
- Docker 컨테이너 내부 root 실행 금지 (비-root 사용자로 구동)
> 환경변수 관리 규칙은 [[env-structure]], 도메인별 적용은 [[nestjs]] · [[flutter]] 참조.
+40
View File
@@ -0,0 +1,40 @@
---
name: testing
description: 세 스택 공통 테스트 규칙 — 테스트 위치/네이밍, 무엇을 테스트할지, 커밋 전 실행. backend Jest, frontend Vitest, flutter flutter_test 기준.
---
# 테스트 규칙
> 템플릿은 각 스택에 테스트 러너가 준비되어 있다 — backend `jest`, frontend `vitest`, flutter `flutter_test`.
> 새 로직을 추가하면 최소한의 테스트를 함께 작성한다.
## 공통 원칙
- **무엇을 테스트하나**: 순수 함수(utils), 서비스/비즈니스 로직, 에러 코드 매핑 분기를 우선한다. 프레임워크 기본 동작은 테스트하지 않는다.
- **테스트는 결정적(deterministic)** 으로 — 외부 네트워크/시간/랜덤에 의존하지 않도록 모킹한다.
- **커밋 전 실행** — 변경 영역의 테스트가 통과해야 한다. ([[checklist]])
- 테스트 설명(it/test 제목)은 한국어로 "무엇을 검증하는지" 명확히 작성한다.
## 스택별 위치·실행
| 스택 | 러너 | 위치·네이밍 | 실행 |
| --- | --- | --- | --- |
| backend | Jest | 단위 `*.spec.ts` (소스 옆), e2e `test/*.e2e-spec.ts` | `npm run test`, `npm run test:e2e` |
| frontend | Vitest | `*.spec.ts` / `*.test.ts` (대상 옆 또는 `__tests__/`) | `npm run test:unit` |
| flutter | flutter_test | `test/` 하위, `*_test.dart` | `flutter test` |
## 예시 (backend 순수 유틸)
```typescript
// shared/utils/format.spec.ts
import { formatSomething } from './format';
describe('formatSomething', () => {
it('빈 입력이면 빈 문자열을 반환한다', () => {
expect(formatSomething('')).toBe('');
});
});
```
> API 응답 래퍼/에러 코드는 세 플랫폼 공유 계약이므로([[response-format]] · [[error-codes]]),
> 매핑 로직을 변경하면 해당 분기 테스트를 추가/갱신한다.
+46
View File
@@ -0,0 +1,46 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Read(//**)",
"Bash(npm run lint:*)",
"Bash(npm run build:*)",
"Bash(npm run test:*)",
"Bash(npm run start:*)",
"Bash(npm ci)",
"Bash(npm install)",
"Bash(pnpm lint:*)",
"Bash(pnpm build:*)",
"Bash(flutter analyze:*)",
"Bash(flutter test:*)",
"Bash(flutter pub get)",
"Bash(dart format:*)",
"Bash(docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development logs:*)",
"Bash(docker compose --env-file .env.production logs:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git add:*)"
],
"ask": [
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(docker compose:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.development)",
"Read(./.env.production)",
"Read(./backend/.env)",
"Read(./backend/.env.development)",
"Read(./backend/.env.production)",
"Read(./frontend/.env)",
"Read(./frontend/.env.development)",
"Read(./frontend/.env.production)",
"Read(./flutter/.env)"
]
},
"env": {
"APP_ENV": "development"
}
}
+84
View File
@@ -0,0 +1,84 @@
---
name: docker
description: Docker Compose 기반 로컬·운영 오케스트레이션 실행/배포 규칙 — 개발/운영 환경 기동, 빌드 여부 판단(--build), 로그 확인, 루트 .env SSOT 주입. 컨테이너를 띄우거나 배포·로그 확인·docker-compose 작업을 할 때 사용한다.
---
> 이 스킬은 `.claude/skills/docker/SKILL.md`에 위치한다.
> 환경변수 SSOT 구조는 [[env-structure]] 참조.
> `.env.development` / `.env.production` 는 커밋된 `.env.*.example` 를 복사해 생성한다.
> (`.gitignore` 는 `.env.*` 를 무시하되 `.env.*.example` 만 추적한다.)
---
## 개발 환경
```bash
# 코드만 수정했을 때 (의존성 변경 없음)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d
# 의존성 변경 시에만 --build
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d --build
# 로그 확인 (예: backend)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development logs -f backend
```
## 운영 환경
```bash
# 운영 환경 배포
sudo docker compose --env-file .env.production up -d --build
# 로그 확인
sudo docker compose --env-file .env.production logs -f backend
```
---
## 자주 쓰는 보조 명령
> 개발 prefix가 길어 아래는 다음 별칭으로 줄여 표기한다.
> 운영은 `docker compose --env-file .env.production` (필요 시 `sudo`) 으로 동일하게 사용한다.
>
> ```bash
> alias dc='docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development'
> ```
```bash
# 상태 확인
dc ps
# 종료 — 컨테이너 중지·삭제 (DB/Redis 데이터 볼륨은 보존)
dc down
# ⚠️ 데이터까지 삭제 — DB/Redis 볼륨 제거 (복구 불가, 신중히)
dc down -v
# 특정 서비스만 재빌드·기동 (예: 의존성 바뀐 backend)
dc up -d --build backend
# 특정 서비스 재시작 (코드 변경은 dev 핫리로드라 보통 불필요)
dc restart backend
# 전체 / 특정 서비스 로그
dc logs -f
dc logs -f backend
# 컨테이너 쉘 접속
dc exec backend sh
# PostgreSQL 접속 (<DB_USER>·<DB_NAME> 은 루트 .env 값으로 치환)
dc exec db psql -U <DB_USER> -d <DB_NAME>
# Redis 접속 (<REDIS_PASSWORD> 은 루트 .env 값)
dc exec redis redis-cli -a <REDIS_PASSWORD>
```
---
## 규칙
- **빌드 최소화**: 의존성(package.json / pubspec 등) 변경이 없으면 `--build` 없이 기동한다.
- **env 파일**: 실행 전 `.env.{APP_ENV}` 존재를 확인한다. 없으면 `.env.{APP_ENV}.example` 을 복사해 생성.
- **컨테이너 비-root 구동**: 모든 서비스 컨테이너는 비-root 사용자로 실행한다 ([[security]] 참조).
- **개발/운영 분리**: 개발은 `docker-compose.yml + docker-compose.dev.yml`, 운영은 `docker-compose.yml` 단독 + `.env.production`.
+246
View File
@@ -0,0 +1,246 @@
---
name: flutter
description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null Safety, feature-first 디렉터리 구조, 작은 위젯 분리, 반응형 UI, Riverpod 상태관리, Dio 인터셉터, flutter_secure_storage 토큰 저장. flutter/ 에서 작업하거나 위젯·프로바이더·모델·API 클라이언트를 만들 때 사용한다.
---
> 이 스킬은 `.claude/skills/flutter/SKILL.md`에 위치한다.
> 사용법: `/flutter [요청 내용]` 또는 flutter/ 작업 시 자동 적용.
> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]]
> Flutter 는 docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립 관리한다 ([[env-structure]] 참조).
---
## 템플릿 기본 제공 골격
> 이 템플릿은 네트워크 계층을 **기본 제공**한다. 새 코드는 이 파일들의 패턴을 재사용한다(같은 역할을 새로 만들지 말 것).
| 제공 항목 | 위치 | 비고 |
| --- | --- | --- |
| Dio 클라이언트 | `lib/core/api/dio_client.dart` | 싱글톤, baseUrl=dotenv `API_URL`(fallback 로컬), 타임아웃 |
| Auth/Error 인터셉터 | `dio_client.dart``InterceptorsWrapper` | onRequest(토큰 첨부)·onResponse(언래핑)·onError(코드 매핑) — 인라인 |
| ErrorLexicon | `lib/core/api/error_lexicon.dart` | 에러 코드 → 한국어 메시지, fallback `SYS_001` ([[error-codes]]) |
| 토큰 저장 | `flutter_secure_storage`, key `jwt_token` | `dio_client` 에서 사용 |
| 상태관리 초기화 | `main.dart``ProviderScope` | Riverpod 사용 준비됨 |
| Dio Provider | `lib/core/api/dio_provider.dart` | 인터셉터 적용된 Dio 를 DI 로 주입 |
| 예제 feature(참조용) | `lib/features/user/` | `data/models`·`data/repositories`·`presentation/providers` 수직 슬라이스 패턴 |
> **확장 시 따르는 규칙** (아래 "프로젝트 구조"는 목표 구조 — 기능 추가 시 점진적으로 채운다):
> - **상태관리**: `features/user` 의 model→repository→provider 패턴을 복제. 위젯에서 `ref.watch(userProvider(id))`
> - **앱 구조**: 라우터 도입 시 `MyApp` 을 `app.dart` 로 분리, `core/{constants,theme,storage}` 채움, 공통 위젯은 `shared/widgets/`
> - **토큰 저장 추상화(선택)**: 필요 시 `core/storage/` 래퍼로 분리
> - **권장 패키지(선택)**: `go_router`·`freezed`·`json_serializable`·`cached_network_image`·`fpdart` 도입 시 `pubspec.yaml` 추가
---
## 언어 & 기본 원칙
- **Dart Sound Null Safety** 완벽 준수 (`?`, `!`, `late` 올바르게 사용)
- `dynamic` 타입 사용 **지양** → 명시적 타입 선언
- 모든 주석은 **한국어**로 작성
---
## 프로젝트 구조
```
lib/
├── main.dart
├── app.dart ← MaterialApp / 라우터 설정
├── core/
│ ├── constants/ ← 상수 (색상, 크기, 텍스트 등)
│ ├── theme/ ← 앱 테마 정의
│ ├── api/ ← Dio 인스턴스(dio_client), 에러 사전(error_lexicon), 인터셉터
│ ├── storage/ ← flutter_secure_storage 래퍼 (선택)
│ └── utils/ ← 순수 유틸 함수
├── features/
│ └── user/
│ ├── data/
│ │ ├── models/ ← JSON 직렬화 모델
│ │ └── repositories/ ← API 호출 구현체
│ ├── domain/
│ │ └── entities/ ← 순수 비즈니스 엔티티
│ └── presentation/
│ ├── pages/ ← 화면 단위 위젯
│ ├── widgets/ ← 재사용 위젯
│ └── providers/ ← 상태 관리
└── shared/
└── widgets/ ← 앱 공통 위젯
```
---
## 위젯 작성 원칙
```dart
// ✅ 좋은 예 — 작은 위젯으로 분리
class UserProfilePage extends StatelessWidget {
const UserProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const _AppBar(),
body: const _ProfileBody(),
);
}
}
class _ProfileBody extends StatelessWidget {
const _ProfileBody();
@override
Widget build(BuildContext context) {
return Column(
children: [
const _AvatarSection(),
const _InfoSection(),
],
);
}
}
```
- 위젯 트리 depth가 깊어지면 **커스텀 위젯으로 분리**
- `build()` 메서드 내 비즈니스 로직 작성 **금지**
- `const` 생성자 적극 활용 (불필요한 리빌드 방지)
---
## 반응형 UI
```dart
// ❌ 하드코딩 금지
Container(width: 375, height: 200)
// ✅ 반응형 처리
Container(
width: MediaQuery.of(context).size.width * 0.9,
child: ...,
)
// ✅ 유연한 레이아웃
Expanded(flex: 2, child: LeftPanel()),
Expanded(flex: 1, child: RightPanel()),
```
- `Expanded`, `Flexible`, `MediaQuery`, `LayoutBuilder` 적극 활용
- 하드코딩 픽셀값 사용 **금지**
- 폰트 크기는 `Theme.of(context).textTheme` 활용
---
## 상태 관리
```dart
// Riverpod 예시 (권장)
@riverpod
Future<User> fetchUser(FetchUserRef ref, int id) async {
final repo = ref.watch(userRepositoryProvider);
return repo.getUser(id);
}
// 위젯에서 사용
class UserWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(fetchUserProvider(userId));
return userAsync.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (e, _) => const ErrorWidget(),
);
}
}
```
- **Riverpod** 권장 (Provider는 레거시로 간주)
- `StatefulWidget`은 애니메이션, 폼 등 로컬 UI 상태에만 제한적 사용
---
## API 통신 (Dio + 인터셉터)
실제 구현(`lib/core/api/dio_client.dart`)은 싱글톤 + `InterceptorsWrapper` 인라인 방식이다. 인터셉터 로직 추가 시 이 파일의 패턴을 그대로 따른다.
```dart
// core/api/dio_client.dart (실제 구조 요약)
class DioClient {
DioClient._internal() {
// flutter_dotenv 로 .env 로드, 미설정 시 로컬 기본값
final baseUrl = dotenv.maybeGet('API_URL') ?? 'http://localhost:3000/api';
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
));
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// jwt_token 읽어 Authorization 헤더 첨부
final token = await _storage.read(key: 'jwt_token');
if (token != null) options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onResponse: (response, handler) {
// success==true → data 언래핑, false → 에러 코드로 reject
handler.next(response);
},
onError: (err, handler) {
// 상태코드/서버 error.code → ErrorLexicon 한국어 메시지 ([[error-codes]] 참조)
handler.next(err);
},
));
}
}
```
> 인터셉터를 별도 클래스(`AuthInterceptor`/`ErrorInterceptor`)로 분리하고 싶다면 리팩터링 가능하나, 현재는 위 인라인 구조가 SSOT다.
---
## 보안 (토큰 저장)
```dart
// flutter_secure_storage 사용
const storage = FlutterSecureStorage();
// 저장 (키 이름은 dio_client 인터셉터와 통일: jwt_token)
await storage.write(key: 'jwt_token', value: token);
// 읽기
final token = await storage.read(key: 'jwt_token');
// 삭제 (로그아웃)
await storage.deleteAll();
```
- JWT, 민감 정보는 반드시 `flutter_secure_storage` 사용
- `SharedPreferences`에 토큰 저장 **금지**
- 추가 보안 규칙은 [[security]] 참조.
---
## 성능 & 최신 트렌드 추가 규칙
- **모델 직렬화**: `json_serializable` + `freezed` 사용 권장 (불변 모델)
- **이미지**: `cached_network_image`로 네트워크 이미지 캐싱
- **목록**: 긴 목록은 `ListView.builder` 사용 (전체 렌더링 금지)
- **에러 핸들링**: `Either<Failure, Success>` 패턴 고려 (`fpdart` 패키지)
- **라우팅**: `go_router` 사용 권장 (딥링크, 중첩 라우팅 지원)
- **환경 분리**: `--dart-define` 또는 `flutter_dotenv`로 dev/prod 분리
---
## 작업 점검 체크리스트
(전체 확장 지점·점검 목록은 [[checklist]])
- [ ] **Null Safety**: `dynamic` 금지, `?`/`!`/`late` 정확히 사용.
- [ ] **위젯**: depth 깊어지면 private 위젯 분리, `const` 적극, `build()` 내 비즈니스 로직 금지.
- [ ] **반응형**: 하드코딩 픽셀 금지 → `MediaQuery`/`Expanded`/`Flexible`.
- [ ] **API**: `dio_client.dart` 의 인터셉터 패턴 재사용. 에러 메시지는 `error_lexicon.dart` 경유.
- [ ] **토큰**: `flutter_secure_storage` + key `jwt_token` 통일. `SharedPreferences` 저장 금지.
- [ ] **상태관리**: 새 기능은 Riverpod 프로바이더/노티파이어로 작성 (`ProviderScope` 는 기본 제공).
- [ ] **구조 보완 시**: `app.dart` 분리, `core/{constants,theme,storage}` 생성, 공통 위젯 `lib/widgets/``shared/widgets/` 이동 고려.
- [ ] **권장 패키지**: 모델(`freezed`/`json_serializable`)·라우팅(`go_router`) 도입 시 `pubspec.yaml` 추가 후 `flutter pub get`.
- [ ] **analyze**: 커밋 전 `flutter analyze` 통과.
- [ ] **env**: `flutter/.env.example``.env` 복사 후 `API_URL` 설정 (Android 에뮬레이터는 `10.0.2.2`).
+197
View File
@@ -0,0 +1,197 @@
---
name: nestjs
description: NestJS 백엔드(backend/) 개발 규칙 — 컨트롤러/서비스/DTO 관심사 분리, 표준 응답 인터셉터, 전역 예외 필터, Swagger, JWT 인증, 캐싱/Rate Limiting/페이지네이션. backend/ 에서 작업하거나 REST API·모듈·DTO·인터셉터·필터를 만들 때 사용한다.
---
> 이 스킬은 `.claude/skills/nestjs/SKILL.md`에 위치한다.
> 사용법: `/nestjs [요청 내용]` 또는 backend/ 작업 시 자동 적용.
> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]]
---
## 템플릿 기본 제공 골격
> 이 템플릿은 아래 인프라를 **기본 제공**한다. 새 코드는 이 파일들의 패턴을 재사용한다(같은 역할을 새로 만들지 말 것).
| 제공 항목 | 위치 | 비고 |
| --- | --- | --- |
| 표준 응답 인터셉터 | `src/common/interceptors/transform.interceptor.ts` | main.ts 전역 등록 — 컨트롤러는 raw 데이터만 반환 |
| 전역 예외 필터 | `src/common/filters/http-exception.filter.ts` | `HttpException``{success:false,error:{code,message}}` ([[error-codes]]) |
| 전역 ValidationPipe | `main.ts` | `whitelist/forbidNonWhitelisted/transform` |
| Swagger | `main.ts``/api-docs` | 모든 엔드포인트 데코레이터 |
| 보안 미들웨어 | `main.ts` | helmet · CORS 화이트리스트 · Rate Limiting(`express-rate-limit`) |
| 로깅 | `shared/logger/winston.config.ts` | `console.log` 대신 사용 |
| 예제 모듈(참조용) | `modules/user`(DTO+Swagger), `modules/health`(`/health`) | 새 모듈 패턴 참고. user 예제는 in-memory 데모 저장소 |
| 순수 유틸 | `shared/utils/` | 순수 함수 분리 위치 |
> **확장 시 따르는 규칙** (기능이 필요해지면 규칙대로 구현):
> - **인증(JWT)**: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/AUTH_003 흐름 검증
> - **DB 영속화**: TypeORM 엔티티/리포지토리 추가 (예제의 in-memory 저장 대체)
> - **페이지네이션**: 목록 API 에 커서/오프셋 적용
> - `common/{guards,decorators,pipes}/` 는 해당 관심사 등장 시 생성 (아래 "모듈 구조"는 목표 구조)
---
## 아키텍처 원칙
### 관심사 분리 (Separation of Concerns)
```
Controller → HTTP 라우팅, 요청/응답 파싱만 담당
Service → 모든 비즈니스 로직 처리
Repository → 데이터 접근 계층 (TypeORM)
DTO → 요청/응답 데이터 형식 정의 및 검증
```
- `Controller`에 비즈니스 로직 작성 **금지**
- `new` 키워드로 수동 객체 생성 **지양** → 의존성 주입(DI) 활용
### 모듈 구조
```
src/
├── modules/
│ └── user/
│ ├── user.module.ts
│ ├── user.controller.ts
│ ├── user.service.ts
│ ├── user.repository.ts ← 커스텀 Repository (선택)
│ └── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
├── common/
│ ├── interceptors/ ← 응답 래핑, 에러 처리
│ ├── filters/ ← 전역 Exception Filter
│ ├── guards/ ← 인증/권한 Guard
│ ├── decorators/ ← 커스텀 데코레이터
│ └── pipes/ ← 전역 Validation Pipe
└── shared/
└── utils/ ← 순수 유틸 함수
```
---
## DTO 작성 규칙
```typescript
import { IsString, IsEmail, IsNotEmpty, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: '사용자 이메일', example: 'user@example.com' })
@IsEmail({}, { message: '올바른 이메일 형식을 입력해 주세요.' })
@IsNotEmpty()
email: string;
@ApiProperty({ description: '비밀번호 (최소 8자)', example: 'password123' })
@IsString()
@MinLength(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' })
password: string;
}
```
- 모든 DTO는 `class-validator` + `class-transformer` 사용
- 에러 메시지는 **한국어**로 작성
- `@ApiProperty()` 반드시 포함 (Swagger 명세용)
---
## 표준 응답 인터셉터
```typescript
// common/interceptors/transform.interceptor.ts
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
success: true,
data: data ?? null,
})),
);
}
}
```
- `main.ts`에서 전역 등록: `app.useGlobalInterceptors(new TransformInterceptor())`
- 실패 응답(`success: false`)은 아래 Exception Filter가 생성한다.
- 응답 포맷 SSOT는 [[response-format]] 참조.
---
## 전역 Exception Filter
```typescript
// common/filters/http-exception.filter.ts
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
// HTTP 상태 → 에러 코드 매핑 후 { success: false, error: { code, message } } 반환
// SYS_001 / AUTH_001 / VAL_001 등 에러 맵은 [[error-codes]] 참조
}
}
```
이 필터는 세 플랫폼 공유 에러 코드 맵의 **단일 진실 공급원(SSOT)** 이다. 메시지 수정 시 [[error-codes]] 의 3개 파일을 함께 갱신한다.
---
## Swagger 필수 적용
```typescript
// main.ts
const config = new DocumentBuilder()
.setTitle('API 문서')
.setDescription('프로젝트 API 명세서')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
```
- 모든 Controller에 `@ApiTags()` 적용
- 모든 엔드포인트에 `@ApiOperation()`, `@ApiResponse()` 작성
- 모든 DTO에 `@ApiProperty()` 작성
---
## 인증 (JWT)
> 📐 **확장 지점.** 이 템플릿은 JWT 인증을 강제하지 않는다(규칙만 제공). **인증이 필요해지면** 아래 규칙대로 구현한다: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/AUTH_003 흐름 검증.
- Access Token: 만료 시간 짧게 (15분 ~ 1시간)
- Refresh Token: HttpOnly Secure 쿠키로 전달
- `@nestjs/jwt` + `@nestjs/passport` 사용
- Guard를 통한 라우트 보호
- 토큰 저장·전송 보안 규칙은 [[security]] 참조.
---
## 성능 & 최신 트렌드 추가 규칙
- **캐싱**: 자주 조회되는 데이터는 `@nestjs/cache-manager` (Redis) 활용 고려
- **Rate Limiting**: 템플릿은 `main.ts` 에서 `express-rate-limit`(초과 시 `SYS_001`)로 API 남용을 방지한다. 라우트 단위 데코레이터 제어가 필요하면 `@nestjs/throttler` 로 전환 고려
- **Pagination**: 목록 API는 반드시 커서 기반 또는 오프셋 페이지네이션 적용
- **Logging**: `winston` 또는 `@nestjs/common Logger` 사용, `console.log` 지양
- **Health Check**: `@nestjs/terminus``/health` 엔드포인트 구성 권장
- **Versioning**: API 버전 관리 (`/v1/`, `/v2/`) 적용 고려
---
## 작업 점검 체크리스트
코드 작성/수정 후 아래를 점검한다. (전체 확장 지점·점검 목록은 [[checklist]])
- [ ] **응답 포맷**: 컨트롤러는 raw 데이터만 반환 — `{success,data}` 래핑은 인터셉터가 담당. 직접 래핑 금지.
- [ ] **에러**: 사용자 노출 실패는 `HttpException` 으로 던져 필터가 코드 매핑하게 한다. 새 코드 추가 시 [[error-codes]] 의 3개 파일 동시 갱신.
- [ ] **DTO**: 모든 입력 DTO 에 `class-validator` + 한국어 메시지 + `@ApiProperty()`.
- [ ] **Swagger**: 새 엔드포인트에 `@ApiTags`/`@ApiOperation`/`@ApiResponse`.
- [ ] **lint**: 커밋 전 `npm run lint` 통과 (Warning 0).
- [ ] **DB 전환 시**: in-memory Map → TypeORM 엔티티/리포지토리로 교체.
- [ ] **인증 추가 시**: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/003 흐름 확인.
- [ ] **env**: DB/Redis/CORS/포트는 루트 `.env` 에서 주입된다. 백엔드 고유 시크릿(`JWT_SECRET` 등)만 `backend/.env.{env}` 에 작성(compose `env_file` 로 로드).
+203
View File
@@ -0,0 +1,203 @@
---
name: vue3
description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API + <script setup>, Props/Emits 타입 정의, Pinia 스토어, Composables+Axios API 호출, Vue Router 코드 스플리팅·가드. frontend/ 에서 작업하거나 컴포넌트·스토어·컴포저블·라우터를 만들 때 사용한다.
---
> 이 스킬은 `.claude/skills/vue3/SKILL.md`에 위치한다.
> 사용법: `/vue3 [요청 내용]` 또는 frontend/ 작업 시 자동 적용.
> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]]
---
## 템플릿 기본 제공 골격
> 이 템플릿은 아래를 **기본 제공**한다. 새 코드는 이 파일들의 패턴을 복제한다(같은 역할을 새로 만들지 말 것).
| 제공 항목 | 위치 | 비고 |
| --- | --- | --- |
| 중앙 Axios 인스턴스 | `src/composables/useApi.ts` | baseURL=`VITE_API_BASE_URL`, `withCredentials` |
| 응답 언래핑 인터셉터 | `useApi.ts` | `{success,data}→data`, 에러 코드 처리 |
| ErrorCodeLexicon | `useApi.ts` 내부 | 에러 코드 → 한국어 메시지 ([[error-codes]]) |
| 도메인 Composable 예제 | `src/composables/useUser.ts` | 새 도메인 API 패턴 참고 |
| 인증 Composable 예제 | `src/composables/useAuth.ts` | 로그인/로그아웃 흐름(쿠키 기반) 시작점 |
| Pinia 스토어 예제 | `src/stores/user.store.ts` | 새 스토어 패턴 참고 |
| 인증 스토어 | `src/stores/auth.store.ts` | 세션 인증 여부(`isLoggedIn`) 관리 |
| 공통 컴포넌트 예제 | `src/components/BaseButton.vue` | 재사용 컴포넌트 기준 패턴(타입 Props/Emits·scoped·rem·a11y) |
| 라우터 | `src/router/index.ts` | lazy import + `auth.store` 연동 `requiresAuth` 가드 |
| env 타입 선언 | `frontend/env.d.ts` | `VITE_API_BASE_URL` |
> **확장 시 따르는 규칙:**
> - **로그인 연동**: 실제 `/auth/login` 엔드포인트 연결 → 성공 시 `useAuth` 가 `authStore.markAuthenticated()` 호출
> - **로그인 페이지**: 추가 시 라우터 가드 리다이렉트를 `{ name: 'login' }` 으로 변경
> - **공통 컴포넌트**: `BaseButton` 패턴을 따라 `src/components/` 에 추가
---
## 컴포넌트 작성 원칙
### 필수 구조
```vue
<script setup lang="ts">
// 1. import
// 2. Props / Emits 정의
// 3. Store / Composable 호출
// 4. 반응형 상태 (ref, reactive, computed)
// 5. 함수 정의
// 6. Lifecycle Hooks
// 7. watch
</script>
<template>
<!-- 시맨틱 HTML 태그 사용 -->
</template>
<style scoped>
/* rem 단위 사용, 인라인 스타일 금지 */
</style>
```
- `Options API` 사용 **금지**`Composition API` + `<script setup>` 필수
- 인라인 스타일(`style=""`) 사용 **금지**`<style scoped>` 사용
- 모든 px 값은 **rem으로 변환** (기준: 16px = 1rem)
---
## Props / Emits 타입 정의
```typescript
// Props
interface Props {
title: string
count?: number
items: string[]
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
})
// Emits
interface Emits {
(e: 'update', value: string): void
(e: 'close'): void
}
const emit = defineEmits<Emits>()
```
---
## 상태 관리 (Pinia)
```typescript
// stores/user.store.ts
export const useUserStore = defineStore('user', () => {
// 상태
const user = ref<User | null>(null)
const isLoggedIn = computed(() => !!user.value)
// 액션
async function fetchUser(id: number) {
// API 호출
}
return { user, isLoggedIn, fetchUser }
})
```
- 도메인별 Store 파일 분리 (`user.store.ts`, `auth.store.ts` 등)
- Store 내 API 호출은 허용, 단 에러 처리는 인터셉터에 위임
---
## API 호출 (Composables + Axios)
```typescript
// composables/useApi.ts — 중앙 Axios 인스턴스
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // env.d.ts 에 타입 선언
withCredentials: true, // HttpOnly 쿠키 전송
})
// 응답 인터셉터 — 표준 응답 언래핑 + 에러 코드 전역 처리
api.interceptors.response.use(
(response) => response.data.data, // { success, data } → data 언래핑
(error) => {
const code = error.response?.data?.error?.code
// 에러 코드별 Toast 알림 처리 ([[error-codes]] 참조)
return Promise.reject(error)
},
)
```
```typescript
// composables/useUser.ts — 도메인별 API Composable
export function useUser() {
const loading = ref(false)
const error = ref<string | null>(null)
async function getUser(id: number) {
loading.value = true
try {
return await api.get(`/users/${id}`)
} finally {
loading.value = false
}
}
return { loading, error, getUser }
}
```
- API 호출 로직은 반드시 `composables/` 폴더에 분리
- 컴포넌트 내 `axios.get()` 직접 호출 **금지**
- `ErrorCodeLexicon` 은 세 플랫폼 공유 에러 맵의 일부 — [[error-codes]] 참조.
---
## 라우터 (Vue Router)
```typescript
// 코드 스플리팅 — 모든 페이지 컴포넌트는 lazy import
const routes = [
{
path: '/dashboard',
component: () => import('@/pages/DashboardPage.vue'),
meta: { requiresAuth: true },
},
]
// 네비게이션 가드 — 인증 처리
router.beforeEach((to) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return '/login'
}
})
```
---
## 성능 & 최신 트렌드 추가 규칙
- **컴포넌트 분리**: 200줄 이상 컴포넌트는 분리 고려
- **`defineAsyncComponent`**: 무거운 컴포넌트는 비동기 로딩 적용
- **`v-memo`**: 반복 렌더링 최적화 필요 시 활용
- **`<Suspense>`**: 비동기 컴포넌트 로딩 상태 처리에 활용
- **환경변수**: 모든 설정값은 `import.meta.env.VITE_*` 형태로 관리 ([[env-structure]] 참조)
- **절대경로**: `@/` 경로 alias 사용 (상대경로 `../../` 지양)
- **접근성(a11y)**: `aria-label`, `role`, `tabindex` 등 기본 접근성 속성 포함
---
## 작업 점검 체크리스트
(전체 확장 지점·점검 목록은 [[checklist]])
- [ ] **API 호출**: 컴포넌트에서 `axios` 직접 호출 금지 → `composables/` 경유.
- [ ] **에러 처리**: 개별 try/catch 로 메시지를 만들지 말고 `useApi` 인터셉터 + [[error-codes]] 에 위임.
- [ ] **컴포넌트**: `<script setup lang="ts">` + `<style scoped>` + rem 단위. 인라인 스타일·Options API 금지.
- [ ] **타입**: Props/Emits 는 인터페이스로 정의.
- [ ] **라우트**: 페이지는 lazy import, 보호 라우트는 `meta.requiresAuth`.
- [ ] **auth 가드(인증 도입 시)**: `router/index.ts``TODO``auth.store.ts` 작성 후 `isLoggedIn` 검사 연결.
- [ ] **lint**: 커밋 전 `npm run lint` + `npm run type-check` 통과.
- [ ] **env**: `VITE_API_BASE_URL` 은 빌드 시 루트 `BACKEND_API_URL` 에서 주입된다 — `frontend/.env` 에 별도 작성 불필요.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+28
View File
@@ -0,0 +1,28 @@
// templates/*.dc.html 의 /*PRETENDARD*/ 토큰을 Pretendard woff2 data URI 로 치환해
// 작업 디렉터리 루트에 최종 .dc.html 아트보드를 생성한다.
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
const WEIGHTS = [
['Regular', 400],
['SemiBold', 600],
['Bold', 700],
];
const faces = WEIGHTS.map(([file, weight]) => {
const b64 = readFileSync(join('fonts', `Pretendard-${file}.subset.woff2`)).toString('base64');
return `@font-face{font-family:'Pretendard';font-style:normal;font-weight:${weight};font-display:swap;src:url(data:font/woff2;base64,${b64}) format('woff2');}`;
}).join('\n');
let count = 0;
for (const name of readdirSync('templates')) {
if (!name.endsWith('.dc.html')) continue;
const src = readFileSync(join('templates', name), 'utf8');
if (!src.includes('/*PRETENDARD*/')) {
console.error(`warn: ${name} 에 /*PRETENDARD*/ 토큰이 없습니다`);
}
writeFileSync(name, src.replace('/*PRETENDARD*/', faces), 'utf8');
count += 1;
console.log(`${name}${(readFileSync(name).length / 1024).toFixed(0)} KB`);
}
console.log(`${count} artboards built`);
+72
View File
@@ -0,0 +1,72 @@
{
"pages": [
{
"id": "page-1",
"name": "홈 (확정)"
},
{
"id": "page-2",
"name": "지난 시안"
}
],
"artboards": [
{
"file": "Main.dc.html",
"title": "서린즈 홈",
"page": "page-1",
"x": 0,
"y": 0,
"w": 1440,
"h": 2420
},
{
"file": "HomeA.dc.html",
"title": "A · 세로 스택형",
"page": "page-2",
"x": 0,
"y": 0,
"w": 1440,
"h": 3000
},
{
"file": "HomeB.dc.html",
"title": "B · 포털 3단형",
"page": "page-2",
"x": 1600,
"y": 0,
"w": 1440,
"h": 2360
},
{
"file": "HomeC.dc.html",
"title": "C · 앱 사이드바형",
"page": "page-2",
"x": 3200,
"y": 0,
"w": 1440,
"h": 2120
}
],
"annotations": [
{
"id": "note-home",
"x": 0,
"y": -520,
"w": 760,
"page": "page-1",
"text": "서린즈 홈 · 확정안 (밀도 그리드형)\n\n게임 랭킹 — 세 갈래로 나눴습니다\n한 칸에 하나씩, 세 랭킹을 나란히 놓았습니다.\n· 계급 랭킹 — 통합 / 시즌\n· 랭크전 랭킹 — 솔로 / 파티 / 클랜 (여기만 RP 점수 열이 있습니다)\n· 클랜 랭킹 — 공식 / 일반\n유저 랭킹은 계급 이미지 + 닉네임, 클랜 랭킹은 클랜 마크 + 클랜명으로만 구성했고 승률·K/D·헤드샷·판수는 모두 뺐습니다. 모든 랭킹에 등락(▲n / ▼n / 변동 없음)을 붙였습니다.\n계급 이미지와 클랜 마크는 실제 에셋 자리로, 지금은 회색 타일에 약식 표장을 그려 뒀습니다. 실제 이미지 규격을 주시면 교체하겠습니다.\n\n검색을 최상단으로\n헤더에서 검색을 빼고 화면 맨 위 히어로로 올렸습니다. 검색창만 덩그러니 올리면 허전해서 세 가지를 함께 뒀습니다.\n· 문구 두 줄 — \"누구의 전적이 궁금하세요?\" 와 무엇을 해주는 곳인지 설명하는 한 줄\n· 최근 검색 칩 — 빈 검색창 아래에 바로 누를 것이 생겨서 첫 화면이 비어 보이지 않습니다\n· 옅은 색면(#FAFBFC) + 아래 경계선 — 히어로가 떠 있는 게 아니라 하나의 층으로 읽힙니다. 아래 그리드와 역할이 분리됩니다\n헤더에는 대신 \"전적 검색\" 메뉴를 넣었습니다.\n\n그 아래 배치 — 커뮤니티와 스트리밍의 자리를 바꿨습니다\n· 이벤트 슬라이드(2칸) + 검색 랭킹(1칸)\n· 게임 랭킹(2칸) + 커뮤니티(1칸)\n· 서든어택 소식 + 서린즈 소식 + 지금 방송 중\n\n지금 방송 중\n· 스트리머 이름 앞에 프로필 아바타를 붙였습니다. 대표 방송은 20px, 아래 목록 세 줄은 18px 원형이고, 히어로의 \"최근에 찾아봤어요\" 칩과 같은 자리표시 방식입니다. 실제 프로필 이미지 규격을 주시면 교체하겠습니다.\n· 빨간 LIVE 점과 글자는 뺐습니다. 이제 아바타 + 닉네임 · 시청자 수만 남습니다.\n\n남은 검토거리\n· 스크롤을 내리면 히어로 검색창이 사라집니다. 헤더에 작은 검색창을 다시 붙여 따라오게 할지 정하면 좋겠습니다.\n· 히어로 오른쪽에 오늘의 요약 수치(분석된 경기 수 등)를 넣는 안도 있는데, 지금은 넣지 않았습니다. 필요하면 말씀해 주세요.\n· 스트리밍이 아랫줄로 내려가면서 그 줄이 소식 목록보다 200px쯤 높아졌습니다. 소식을 6건에서 8건으로 늘려 높이를 맞출지 정하면 좋겠습니다.\n\n회색 사각형은 썸네일·스크린샷 자리, 닉네임·수치·공지 제목은 예시 데이터입니다."
},
{
"id": "note-past",
"x": 0,
"y": -300,
"w": 700,
"page": "page-2",
"text": "지난 시안 (참고용)\n\n고르지 않은 세 배치입니다. 나중에 되돌아볼 일이 있을까 싶어 남겨 뒀습니다. 지워도 괜찮으면 말씀해 주세요.\n이벤트 배너와 커뮤니티 글 종류는 세 안에도 모두 반영되어 있습니다."
}
],
"launch": {
"view": "canvas",
"page": "page-1"
}
}
Binary file not shown.
Binary file not shown.
+198
View File
@@ -0,0 +1,198 @@
// 게임 랭킹 섹션(계급 / 랭크전 / 클랜)을 데이터로부터 생성해 parts/ranking.html 에 쓴다.
// 행이 30줄이라 손으로 쓰지 않고 여기서 찍어낸다.
import { writeFileSync } from 'node:fs';
// 계급 표장 — 실제 계급 이미지가 들어올 자리의 약식 표기
const INSIGNIA = {
chev: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg>',
dia: '<svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg>',
star: '<svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg>',
};
// 클랜 마크 — 실제 클랜 마크가 들어올 자리
const MARK = {
shield: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg>',
hex: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg>',
};
// 등락 셀
function move(n) {
if (n === 0) return '<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>';
const up = n > 0;
const color = up ? '#12805C' : '#C4453D';
const path = up ? 'M6 2l4 7H2z' : 'M6 10L2 3h8z';
return `<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="${color}"><path d="${path}"></path></svg><span style="font-size: 0.75rem; color: ${color};">${Math.abs(n)}</span></span>`;
}
const rankNum = (i) =>
i < 3
? `<span style="font-size: 0.875rem; font-weight: 700;">${i + 1}</span>`
: `<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">${i + 1}</span>`;
const NAME = '<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">';
// 유저 랭킹 한 블록 (RP 열은 rp 값이 있을 때만)
function userList(rows, withRp) {
const cols = withRp
? '18px 26px minmax(0, 1fr) 48px 38px'
: '18px 26px minmax(0, 1fr) 38px';
return rows
.map((r, i) => {
const last = i === rows.length - 1;
const border = last ? '' : ' border-bottom: 1px solid #F3F5F7;';
const rp = withRp
? `<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">${r.rp}</span>`
: '';
return ` <div style="display: grid; grid-template-columns: ${cols}; align-items: center; gap: 0.5rem; height: 46px;${border}">
${rankNum(i)}
<span class="d-badge">${INSIGNIA[r.ins]}</span>
${NAME}${r.name}</span>
${rp}${move(r.mv)}
</div>`;
})
.join('\n');
}
function clanList(rows) {
const cols = '18px 28px minmax(0, 1fr) 38px';
return rows
.map((r, i) => {
const last = i === rows.length - 1;
const border = last ? '' : ' border-bottom: 1px solid #F3F5F7;';
return ` <div style="display: grid; grid-template-columns: ${cols}; align-items: center; gap: 0.5rem; height: 46px;${border}">
${rankNum(i)}
<span class="d-mark">${MARK[r.mark]}</span>
${NAME}${r.name}</span>
${move(r.mv)}
</div>`;
})
.join('\n');
}
const tab = (label, on) => `<span class="d-tab${on ? ' d-tab-on' : ''}">${label}</span>`;
// 표 머리글
function head(withRp, unit) {
const cols = withRp
? '18px 26px minmax(0, 1fr) 48px 38px'
: `18px ${unit} minmax(0, 1fr) 38px`;
const rp = withRp ? '\n <span class="d-th" style="text-align: right;">RP</span>' : '';
const who = unit === '28px' ? '클랜' : '유저';
return ` <div style="display: grid; grid-template-columns: ${cols}; align-items: center; gap: 0.5rem; height: 32px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">#</span><span></span><span class="d-th">${who}</span>${rp}
<span class="d-th" style="text-align: right;">등락</span>
</div>`;
}
const 계급 = [
{ name: '달빛조각사', ins: 'chev', mv: 2 },
{ name: '칼든햄스터', ins: 'star', mv: -1 },
{ name: '새벽두시', ins: 'chev', mv: 4 },
{ name: '무명연대장', ins: 'dia', mv: 0 },
{ name: '한밤의저격수', ins: 'dia', mv: 3 },
{ name: '조용한총잡이', ins: 'chev', mv: -2 },
{ name: '야간부대장', ins: 'dia', mv: 0 },
{ name: '헤드샷장인', ins: 'dia', mv: 5 },
{ name: '삼보급단골', ins: 'chev', mv: -3 },
{ name: '웨어하우스지박령', ins: 'star', mv: 1 },
];
const 랭크전 = [
{ name: '헤드샷장인', ins: 'star', rp: '2,486', mv: 1 },
{ name: '달빛조각사', ins: 'chev', rp: '2,451', mv: -1 },
{ name: '삼보급단골', ins: 'dia', rp: '2,398', mv: 6 },
{ name: '칼든햄스터', ins: 'star', rp: '2,344', mv: 0 },
{ name: '웨어하우스지박령', ins: 'dia', rp: '2,301', mv: 2 },
{ name: '새벽두시', ins: 'chev', rp: '2,276', mv: -3 },
{ name: '조용한총잡이', ins: 'dia', rp: '2,240', mv: 0 },
{ name: '한밤의저격수', ins: 'dia', rp: '2,205', mv: 4 },
{ name: '야간부대장', ins: 'chev', rp: '2,181', mv: -2 },
{ name: '무명연대장', ins: 'dia', rp: '2,154', mv: 1 },
];
const 클랜 = [
{ name: '무명연대', mark: 'shield', mv: 0 },
{ name: '새벽클랜', mark: 'hex', mv: 2 },
{ name: '야간부대', mark: 'shield', mv: -1 },
{ name: '정예사격단', mark: 'hex', mv: 3 },
{ name: '삼보급수호대', mark: 'shield', mv: 0 },
{ name: '헤드샷연구소', mark: 'hex', mv: 4 },
{ name: '크로스카운터', mark: 'shield', mv: -2 },
{ name: '웨어하우스단', mark: 'hex', mv: 0 },
{ name: '새벽정찰대', mark: 'shield', mv: 1 },
{ name: '삼보급기동대', mark: 'hex', mv: -3 },
];
// 페이지 이동 — 순위 구간을 세 랭킹이 함께 넘긴다
const pageBtn = (label, on) =>
on
? `<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; background: #14161A; font-size: 0.875rem; font-weight: 700; color: #FFFFFF;">${label}</span>`
: `<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">${label}</span>`;
const chev = (dir, dim) =>
`<span class="d-arw"${dim ? ' style="opacity: 0.4;"' : ''}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="${dir === 'prev' ? 'M14.5 5l-7 7 7 7' : 'M9.5 5l7 7-7 7'}"></path></svg></span>`;
const pager = `
<!-- 순위 구간 이동 — 세 랭킹이 같은 구간을 함께 본다 -->
<div style="display: flex; align-items: center; gap: 0.75rem; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid #EDEFF2;">
<span style="font-size: 0.8125rem; color: #9BA1A9;">1 ~ 10위 · 전체 100위</span>
<div style="flex-grow: 1;"></div>
${chev('prev', true)}
<div style="display: flex; align-items: center; gap: 0.125rem;">
${pageBtn('1', true)}
${pageBtn('2', false)}
${pageBtn('3', false)}
${pageBtn('4', false)}
${pageBtn('5', false)}
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 26px; height: 34px; font-size: 0.875rem; color: #9BA1A9;">…</span>
${pageBtn('10', false)}
</div>
${chev('next', false)}
</div>`;
const html = ` <!-- 게임 랭킹 (2칸) — 계급 / 랭크전 / 클랜 세 갈래를 나란히 -->
<section style="grid-column: span 2;">
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">게임 랭킹</h2>
<span style="margin-left: 0.75rem; font-size: 0.8125rem; color: #9BA1A9;">매일 오전 8시 갱신</span>
<div style="flex-grow: 1;"></div><span class="d-more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem;">
<!-- 계급 랭킹 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">계급 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
${tab('통합', true)}${tab('시즌', false)}
</div>
${head(false, '26px')}
${userList(계급, false)}
</div>
<!-- 랭크전 랭킹 — RP 점수를 함께 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">랭크전 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
${tab('솔로', true)}${tab('파티', false)}${tab('클랜', false)}
</div>
${head(true, '26px')}
${userList(랭크전, true)}
</div>
<!-- 클랜 랭킹 — 클랜 마크 + 클랜명 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">클랜 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
${tab('공식', true)}${tab('일반', false)}
</div>
${head(false, '28px')}
${clanList(클랜)}
</div>
</div>
${pager}
</section>
`;
writeFileSync('parts/ranking.html', html, 'utf8');
console.log('parts/ranking.html written —', html.length, 'chars');
+245
View File
@@ -0,0 +1,245 @@
<!-- 게임 랭킹 (2칸) — 계급 / 랭크전 / 클랜 세 갈래를 나란히 -->
<section style="grid-column: span 2;">
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">게임 랭킹</h2>
<span style="margin-left: 0.75rem; font-size: 0.8125rem; color: #9BA1A9;">매일 오전 8시 갱신</span>
<div style="flex-grow: 1;"></div><span class="d-more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem;">
<!-- 계급 랭킹 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">계급 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
<span class="d-tab d-tab-on">통합</span><span class="d-tab">시즌</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 32px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">#</span><span></span><span class="d-th">유저</span>
<span class="d-th" style="text-align: right;">등락</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽두시</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">4</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대장</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">한밤의저격수</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">조용한총잡이</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">야간부대장</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">헤드샷장인</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">5</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급단골</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">10</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">웨어하우스지박령</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
</div>
<!-- 랭크전 랭킹 — RP 점수를 함께 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">랭크전 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
<span class="d-tab d-tab-on">솔로</span><span class="d-tab">파티</span><span class="d-tab">클랜</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 32px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">#</span><span></span><span class="d-th">유저</span>
<span class="d-th" style="text-align: right;">RP</span>
<span class="d-th" style="text-align: right;">등락</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">헤드샷장인</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,486</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,451</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급단골</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,398</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">6</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,344</span><span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">웨어하우스지박령</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,301</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽두시</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,276</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">조용한총잡이</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,240</span><span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">한밤의저격수</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,205</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">4</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">야간부대장</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,181</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">10</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대장</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,154</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
</div>
<!-- 클랜 랭킹 — 클랜 마크 + 클랜명 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">클랜 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
<span class="d-tab d-tab-on">공식</span><span class="d-tab">일반</span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 32px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">#</span><span></span><span class="d-th">클랜</span>
<span class="d-th" style="text-align: right;">등락</span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽클랜</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">야간부대</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">정예사격단</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급수호대</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">헤드샷연구소</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">4</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">크로스카운터</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">웨어하우스단</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽정찰대</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">10</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급기동대</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">3</span></span>
</div>
</div>
</div>
<!-- 순위 구간 이동 — 세 랭킹이 같은 구간을 함께 본다 -->
<div style="display: flex; align-items: center; gap: 0.75rem; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid #EDEFF2;">
<span style="font-size: 0.8125rem; color: #9BA1A9;">1 ~ 10위 · 전체 100위</span>
<div style="flex-grow: 1;"></div>
<span class="d-arw" style="opacity: 0.4;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5l-7 7 7 7"></path></svg></span>
<div style="display: flex; align-items: center; gap: 0.125rem;">
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; background: #14161A; font-size: 0.875rem; font-weight: 700; color: #FFFFFF;">1</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">2</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">3</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">4</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">5</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 26px; height: 34px; font-size: 0.875rem; color: #9BA1A9;"></span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">10</span>
</div>
<span class="d-arw"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5l7 7-7 7"></path></svg></span>
</div>
</section>
File diff suppressed because one or more lines are too long
+335
View File
@@ -0,0 +1,335 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
/*PRETENDARD*/
/* 안 A — 세로 스택형: 검색 히어로 아래로 섹션을 한 줄씩 쌓는다 */
body { margin: 0; }
.h-root, .h-root * { box-sizing: border-box; }
.h-root {
font-family: "Pretendard", -apple-system, "Apple SD Gothic Neo", sans-serif;
-webkit-font-smoothing: antialiased;
font-variant-numeric: tabular-nums;
letter-spacing: -0.015em;
}
a { color: #0866FF; text-decoration: none; }
a:hover { color: #0450CC; }
.h2 { margin: 0; font-size: 1.25rem; font-weight: 700; letter-spacing: -0.035em; }
.th { font-size: 0.75rem; font-weight: 500; color: #9BA1A9; }
.tab { display: inline-flex; align-items: center; height: 32px; padding: 0 0.875rem; border-radius: 999px; background: #F5F6F8; font-size: 0.8125rem; font-weight: 500; color: #5B6169; }
.tab-on { background: #14161A; font-weight: 600; color: #FFFFFF; }
.more { font-size: 0.8125rem; font-weight: 600; color: #5B6169; }
.clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.arw { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 1px solid #E4E8ED; border-radius: 999px; background: #FFFFFF; cursor: pointer; }
</style>
</helmet>
<div class="h-root" style="width: 1440px; min-height: 3000px; background: #FFFFFF; color: #14161A;">
<header style="display: flex; align-items: center; gap: 2rem; height: 76px; padding: 0 48px; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.625rem;">
<div style="width: 32px; height: 32px; border-radius: 11px; background: #0866FF; display: flex; align-items: center; justify-content: center;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"></circle><path d="M19.5 19.5L16 16"></path></svg>
</div>
<span style="font-size: 1.25rem; font-weight: 700; letter-spacing: -0.035em;">서린즈</span>
</div>
<nav style="display: flex; align-items: center; gap: 1.625rem; margin-left: 1rem; font-size: 0.9375rem;">
<span style="font-weight: 700;"></span>
<span style="font-weight: 500; color: #5B6169;">전적 검색</span>
<span style="font-weight: 500; color: #5B6169;">랭킹</span>
<span style="font-weight: 500; color: #5B6169;">커뮤니티</span>
<span style="font-weight: 500; color: #5B6169;">스트리밍</span>
<span style="font-weight: 500; color: #5B6169;">소식</span>
</nav>
<div style="flex-grow: 1;"></div>
<button style="height: 42px; padding: 0 1.25rem; border: none; border-radius: 999px; background: #F2F4F7; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">로그인</button>
</header>
<!-- 검색 -->
<section style="display: flex; flex-direction: column; align-items: center; padding: 48px 48px 44px;">
<h1 style="margin: 0; font-size: 2.25rem; line-height: 1.25; font-weight: 700; letter-spacing: -0.045em;">누구의 전적이 궁금하세요?</h1>
<p style="margin: 0.75rem 0 0; font-size: 0.9375rem; line-height: 1.6; color: #5B6169;">닉네임만 알려주시면 최근 경기부터 자주 쓰는 무기까지 정리해 드릴게요.</p>
<div style="display: flex; align-items: center; gap: 0.875rem; width: 100%; max-width: 680px; height: 64px; margin-top: 1.625rem; padding: 0 0.5rem 0 1.375rem; background: #FFFFFF; border: 1px solid #E4E8ED; border-radius: 999px; box-shadow: 0 1px 2px rgba(20, 22, 26, 0.04), 0 12px 32px rgba(20, 22, 26, 0.07);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#9BA1A9" stroke-width="2.2" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"></circle><path d="M19.5 19.5L16 16"></path></svg>
<span style="flex-grow: 1; font-size: 1rem; color: #9BA1A9;">닉네임을 입력해 보세요</span>
<button style="height: 48px; padding: 0 1.625rem; border: none; border-radius: 999px; background: #0866FF; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #FFFFFF; letter-spacing: -0.02em; cursor: pointer;">찾아보기</button>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem; margin-top: 1.25rem;">
<span style="font-size: 0.875rem; color: #9BA1A9; margin-right: 0.25rem;">최근에 찾아봤어요</span>
<span style="display: inline-flex; align-items: center; gap: 0.5rem; height: 36px; padding: 0 0.875rem 0 0.375rem; border-radius: 999px; background: #F5F6F8; font-size: 0.875rem; font-weight: 600;"><span style="width: 24px; height: 24px; border-radius: 999px; background: #DDE4EE;"></span>달빛조각사</span>
<span style="display: inline-flex; align-items: center; gap: 0.5rem; height: 36px; padding: 0 0.875rem 0 0.375rem; border-radius: 999px; background: #F5F6F8; font-size: 0.875rem; font-weight: 600;"><span style="width: 24px; height: 24px; border-radius: 999px; background: #E5E0F0;"></span>칼든햄스터</span>
<span style="display: inline-flex; align-items: center; gap: 0.5rem; height: 36px; padding: 0 0.875rem 0 0.375rem; border-radius: 999px; background: #F5F6F8; font-size: 0.875rem; font-weight: 600;"><span style="width: 24px; height: 24px; border-radius: 999px; background: #E7E3D8;"></span>새벽두시</span>
</div>
</section>
<!-- 이벤트 슬라이드 -->
<section style="padding: 0 48px 48px;">
<div style="display: flex; align-items: center; margin-bottom: 1rem;">
<h2 class="h2">이벤트</h2>
<div style="flex-grow: 1;"></div>
<span class="more">전체 보기</span>
</div>
<!-- 배너는 제목이 이미지 안에 들어 있는 가로형(720:248). 이미지를 그대로 살리고 설명은 아래 한 줄로 뺀다. -->
<div style="border: 1px solid #EDEFF2; border-radius: 24px; overflow: hidden;">
<img src="20260903074208.jpg" alt="서든 페스티벌 같이 갈래?" style="display: block; width: 100%; aspect-ratio: 720 / 248; object-fit: cover;">
</div>
<div style="display: flex; align-items: center; gap: 0.875rem; height: 60px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">시즌 이벤트</span>
<span style="width: 1px; height: 12px; background: #E4E8ED;"></span>
<span style="font-size: 1rem; font-weight: 600;">서든 페스티벌 같이 갈래?</span>
<span style="font-size: 0.875rem; color: #9BA1A9;">9월 1일 ~ 9월 30일</span>
<div style="flex-grow: 1;"></div>
<span style="width: 22px; height: 5px; border-radius: 999px; background: #14161A;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="font-size: 0.8125rem; color: #9BA1A9; margin: 0 0.25rem 0 0.5rem;">1 / 5</span>
<span class="arw"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5l-7 7 7 7"></path></svg></span>
<span class="arw"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5l7 7-7 7"></path></svg></span>
</div>
</section>
<!-- 게임 랭킹 + 검색 랭킹 -->
<section style="display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 2.5rem; padding: 0 48px 48px;">
<div>
<div style="display: flex; align-items: center;">
<h2 class="h2">게임 랭킹</h2>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-left: 1.25rem;">
<span class="tab tab-on">전체</span>
<span class="tab">폭파미션</span>
<span class="tab">팀 데스매치</span>
<span class="tab">클랜전</span>
</div>
<div style="flex-grow: 1;"></div>
<span class="more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: 44px minmax(0, 1fr) 96px 80px 88px 88px; align-items: center; gap: 0.75rem; height: 36px; margin-top: 1rem; border-bottom: 1px solid #E4E8ED;">
<span class="th">순위</span><span class="th">유저</span>
<span class="th" style="text-align: right;">승률</span>
<span class="th" style="text-align: right;">K/D</span>
<span class="th" style="text-align: right;">헤드샷</span>
<span class="th" style="text-align: right;">판수</span>
</div>
<div style="display: grid; grid-template-columns: 44px minmax(0, 1fr) 96px 80px 88px 88px; align-items: center; gap: 0.75rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 700;">1</span>
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0;"><span style="width: 34px; height: 34px; border-radius: 999px; background: #DDE4EE; flex-shrink: 0;"></span><span style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</span><span style="font-size: 0.8125rem; color: #9BA1A9;">병장 3호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">78.4%</span>
<span style="text-align: right; font-size: 0.9375rem;">2.14</span>
<span style="text-align: right; font-size: 0.9375rem;">41.2%</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">1,284</span>
</div>
<div style="display: grid; grid-template-columns: 44px minmax(0, 1fr) 96px 80px 88px 88px; align-items: center; gap: 0.75rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 700;">2</span>
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0;"><span style="width: 34px; height: 34px; border-radius: 999px; background: #E5E0F0; flex-shrink: 0;"></span><span style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</span><span style="font-size: 0.8125rem; color: #9BA1A9;">소위 1호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">74.9%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.96</span>
<span style="text-align: right; font-size: 0.9375rem;">38.7%</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">2,041</span>
</div>
<div style="display: grid; grid-template-columns: 44px minmax(0, 1fr) 96px 80px 88px 88px; align-items: center; gap: 0.75rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 700;">3</span>
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0;"><span style="width: 34px; height: 34px; border-radius: 999px; background: #E7E3D8; flex-shrink: 0;"></span><span style="font-size: 0.9375rem; font-weight: 600;">새벽두시</span><span style="font-size: 0.8125rem; color: #9BA1A9;">병장 1호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">71.2%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.88</span>
<span style="text-align: right; font-size: 0.9375rem;">44.1%</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">986</span>
</div>
<div style="display: grid; grid-template-columns: 44px minmax(0, 1fr) 96px 80px 88px 88px; align-items: center; gap: 0.75rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 600; color: #9BA1A9;">4</span>
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0;"><span style="width: 34px; height: 34px; border-radius: 999px; background: #D8E6E2; flex-shrink: 0;"></span><span style="font-size: 0.9375rem; font-weight: 600;">무명연대장</span><span style="font-size: 0.8125rem; color: #9BA1A9;">중사 2호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">69.5%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.74</span>
<span style="text-align: right; font-size: 0.9375rem;">33.9%</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">3,102</span>
</div>
<div style="display: grid; grid-template-columns: 44px minmax(0, 1fr) 96px 80px 88px 88px; align-items: center; gap: 0.75rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 600; color: #9BA1A9;">5</span>
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0;"><span style="width: 34px; height: 34px; border-radius: 999px; background: #EDDFE2; flex-shrink: 0;"></span><span style="font-size: 0.9375rem; font-weight: 600;">한밤의저격수</span><span style="font-size: 0.8125rem; color: #9BA1A9;">상사 1호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">68.1%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.71</span>
<span style="text-align: right; font-size: 0.9375rem;">47.3%</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">742</span>
</div>
<div style="display: grid; grid-template-columns: 44px minmax(0, 1fr) 96px 80px 88px 88px; align-items: center; gap: 0.75rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 600; color: #9BA1A9;">6</span>
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0;"><span style="width: 34px; height: 34px; border-radius: 999px; background: #E4E8ED; flex-shrink: 0;"></span><span style="font-size: 0.9375rem; font-weight: 600;">조용한총잡이</span><span style="font-size: 0.8125rem; color: #9BA1A9;">병장 2호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">66.7%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.68</span>
<span style="text-align: right; font-size: 0.9375rem;">36.4%</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">1,530</span>
</div>
<button style="width: 100%; height: 48px; margin-top: 1rem; border: none; border-radius: 999px; background: #F5F6F8; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">랭킹 100위까지 보기</button>
</div>
<div>
<div style="display: flex; align-items: center;">
<h2 class="h2">검색 랭킹</h2>
<div style="flex-grow: 1;"></div>
<span style="font-size: 0.8125rem; color: #9BA1A9;">18:20 기준</span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 36px; margin-top: 1rem; border-bottom: 1px solid #E4E8ED;">
<span class="th">순위</span><span class="th">닉네임</span><span class="th" style="text-align: right;">변동</span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span><span class="clip" style="font-size: 0.9375rem;">달빛조각사</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span><span class="clip" style="font-size: 0.9375rem;">칼든햄스터</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.8125rem; color: #C4453D;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span><span class="clip" style="font-size: 0.9375rem;">새벽두시</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">4</span></span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span><span class="clip" style="font-size: 0.9375rem;">무명연대장</span>
<span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span><span class="clip" style="font-size: 0.9375rem;">한밤의저격수</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span><span class="clip" style="font-size: 0.9375rem;">조용한총잡이</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.8125rem; color: #C4453D;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span><span class="clip" style="font-size: 0.9375rem;">야간부대장</span>
<span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 28px minmax(0, 1fr) 60px; align-items: center; gap: 0.75rem; height: 44px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span><span class="clip" style="font-size: 0.9375rem;">헤드샷장인</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">5</span></span>
</div>
</div>
</section>
<!-- 소식 2단 -->
<section style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2.5rem; padding: 0 48px 48px;">
<div>
<div style="display: flex; align-items: center;">
<h2 class="h2">서든어택 소식</h2><div style="flex-grow: 1;"></div><span class="more">더 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.875rem;">
<span class="tab tab-on">공지사항</span><span class="tab">업데이트</span>
</div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">9월 정기점검 안내 (09.04 02:00~08:00)</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.02</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">비매너 이용자 제재 결과 안내</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">가을 시즌 랭크전 일정 공지</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.29</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">신규 맵 컨테이너 야드 추가</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.28</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">AK-47 반동 수치 조정 안내</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.26</span></div>
</div>
</div>
<div>
<div style="display: flex; align-items: center;">
<h2 class="h2">서린즈 소식</h2><div style="flex-grow: 1;"></div><span class="more">더 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.875rem;">
<span class="tab tab-on">공지사항</span><span class="tab">업데이트</span>
</div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">맵별 승률 통계를 새로 열었어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.03</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">전적 갱신 속도가 두 배 빨라졌어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">즐겨찾기한 유저 알림 기능 추가</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.30</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">8월 서버 점검 결과 보고</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.27</span></div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 48px; border-bottom: 1px solid #F3F5F7;"><span style="width: 7px; height: 7px; flex-shrink: 0;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">커뮤니티 이용 규칙을 정리했어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.24</span></div>
</div>
</div>
</section>
<!-- 스트리밍 -->
<section style="padding: 0 48px 48px;">
<div style="display: flex; align-items: center;">
<h2 class="h2">지금 방송 중</h2>
<span style="margin-left: 0.875rem; font-size: 0.875rem; color: #9BA1A9;">시청자 2,924명</span>
<div style="flex-grow: 1;"></div><span class="more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; margin-top: 1.125rem;">
<div>
<div style="aspect-ratio: 16 / 9; border-radius: 16px; background: #E4E8ED;"></div>
<div class="clip" style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">삼보급 스나 연습</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.3125rem;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; font-weight: 600; color: #E8443A;">LIVE</span><span style="font-size: 0.8125rem; color: #9BA1A9;">달빛조각사 · 1,204명</span></div>
</div>
<div>
<div style="aspect-ratio: 16 / 9; border-radius: 16px; background: #EAE6E0;"></div>
<div class="clip" style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">클랜전 정기 리그 중계</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.3125rem;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; font-weight: 600; color: #E8443A;">LIVE</span><span style="font-size: 0.8125rem; color: #9BA1A9;">무명연대 · 862명</span></div>
</div>
<div>
<div style="aspect-ratio: 16 / 9; border-radius: 16px; background: #E2E9E6;"></div>
<div class="clip" style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">초보 탈출 폭파미션 강의</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.3125rem;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; font-weight: 600; color: #E8443A;">LIVE</span><span style="font-size: 0.8125rem; color: #9BA1A9;">새벽두시 · 517명</span></div>
</div>
<div>
<div style="aspect-ratio: 16 / 9; border-radius: 16px; background: #E8E4EC;"></div>
<div class="clip" style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">시청자 참여 한 판</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.3125rem;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; font-weight: 600; color: #E8443A;">LIVE</span><span style="font-size: 0.8125rem; color: #9BA1A9;">한밤의저격수 · 341명</span></div>
</div>
</div>
</section>
<!-- 커뮤니티 -->
<section style="padding: 0 48px 48px;">
<div style="display: flex; align-items: center;">
<h2 class="h2">커뮤니티</h2>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-left: 1.25rem;">
<span class="tab tab-on">인기</span><span class="tab">최신</span><span class="tab">클립</span><span class="tab">공략</span><span class="tab">클랜원 모집</span>
</div>
<div style="flex-grow: 1;"></div><span class="more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; margin-top: 1.125rem;">
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #E4E8ED; overflow: hidden;">
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.5rem; height: 46px; padding: 0 0.875rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 22px; height: 22px; border-radius: 999px; background: #C9D2DD;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.8125rem; font-weight: 600; color: #FFFFFF;">달빛조각사</span><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.3A4.2 4.2 0 0112 8a4.2 4.2 0 017 2.7c0 4.9-7 9.3-7 9.3z"></path></svg><span style="font-size: 0.8125rem; font-weight: 600; color: #FFFFFF;">312</span></div>
</div>
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #EAE6E0; overflow: hidden;">
<div style="position: absolute; top: 12px; right: 12px;"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="3" width="13" height="13" rx="3"></rect><path d="M16 19.5A2.5 2.5 0 0113.5 22H6a3 3 0 01-3-3V9.5"></path></svg></div>
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.5rem; height: 46px; padding: 0 0.875rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 22px; height: 22px; border-radius: 999px; background: #D8D2C8;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.8125rem; font-weight: 600; color: #FFFFFF;">칼든햄스터</span><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.3A4.2 4.2 0 0112 8a4.2 4.2 0 017 2.7c0 4.9-7 9.3-7 9.3z"></path></svg><span style="font-size: 0.8125rem; font-weight: 600; color: #FFFFFF;">274</span></div>
</div>
<!-- 사진 없는 글 — 본문이 곧 썸네일이 된다 -->
<div style="display: flex; flex-direction: column; aspect-ratio: 1 / 1; padding: 22px; border-radius: 18px; background: #F5F6F8;">
<p style="margin: 0; font-size: 1.0625rem; line-height: 1.6; font-weight: 500;">폭파 삼보급에서 A 롱각 잡을 때 다들 어디 서세요? 요즘 자꾸 먼저 죽습니다.</p>
<div style="flex-grow: 1;"></div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="width: 22px; height: 22px; border-radius: 999px; background: #DDE4EE;"></span>
<span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.8125rem; font-weight: 600;">새벽두시</span>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.3A4.2 4.2 0 0112 8a4.2 4.2 0 017 2.7c0 4.9-7 9.3-7 9.3z"></path></svg>
<span style="font-size: 0.8125rem; font-weight: 600; color: #5B6169;">198</span>
</div>
</div>
<!-- 클립(영상) 글 -->
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #E8E4EC; overflow: hidden;">
<div style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: flex; align-items: center; justify-content: center; width: 52px; height: 52px; border-radius: 999px; background: rgba(20, 22, 26, 0.55);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M8 5.5l11 6.5-11 6.5z"></path></svg>
</div>
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.5rem; height: 46px; padding: 0 0.875rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 22px; height: 22px; border-radius: 999px; background: #D5CEDC;"></span><span class="clip" style="flex-grow: 1; min-width: 0; font-size: 0.8125rem; font-weight: 600; color: #FFFFFF;">무명연대장</span><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.3A4.2 4.2 0 0112 8a4.2 4.2 0 017 2.7c0 4.9-7 9.3-7 9.3z"></path></svg><span style="font-size: 0.8125rem; font-weight: 600; color: #FFFFFF;">163</span></div>
</div>
</div>
<button style="width: 100%; height: 48px; margin-top: 1.25rem; border: none; border-radius: 999px; background: #F5F6F8; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">게시글 더 보기</button>
</section>
<footer style="display: flex; align-items: center; gap: 1.5rem; padding: 32px 48px 48px; border-top: 1px solid #EDEFF2; font-size: 0.875rem; color: #9BA1A9;">
<span style="font-weight: 600; color: #5B6169;">서린즈</span><span>이용약관</span><span>개인정보 처리방침</span><span>문의하기</span>
<div style="flex-grow: 1;"></div><span>넥슨 공개 API 기반 · 서든어택 비공식 서비스</span>
</footer>
</div>
</x-dc>
</body>
</html>
+547
View File
@@ -0,0 +1,547 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
/*PRETENDARD*/
/* 홈 B — 포털 3단: 좌 랭킹 / 중앙 검색·소식·커뮤니티 / 우 스트리밍·이벤트 */
body { margin: 0; }
.p-root, .p-root * { box-sizing: border-box; }
.p-root {
font-family: "Pretendard", -apple-system, "Apple SD Gothic Neo", sans-serif;
-webkit-font-smoothing: antialiased;
font-variant-numeric: tabular-nums;
letter-spacing: -0.015em;
}
a { color: #0866FF; text-decoration: none; }
a:hover { color: #0450CC; }
.p-h2 { margin: 0; font-size: 1.0625rem; font-weight: 700; letter-spacing: -0.03em; }
.p-th { font-size: 0.75rem; font-weight: 500; color: #9BA1A9; }
.p-tab { display: inline-flex; align-items: center; height: 30px; padding: 0 0.8125rem; border-radius: 999px; background: #F5F6F8; font-size: 0.8125rem; font-weight: 500; color: #5B6169; }
.p-tab-on { background: #14161A; font-weight: 600; color: #FFFFFF; }
.p-more { font-size: 0.8125rem; font-weight: 600; color: #5B6169; }
/* 한 줄 제목 자르기 */
.p-clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.p-arw { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 1px solid #E4E8ED; border-radius: 999px; background: #FFFFFF; cursor: pointer; }
</style>
</helmet>
<div class="p-root" style="width: 1440px; min-height: 2360px; background: #FFFFFF; color: #14161A;">
<!-- 상단 내비게이션 (검색 통합) -->
<header style="display: flex; align-items: center; gap: 1.75rem; height: 72px; padding: 0 40px; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.625rem;">
<div style="width: 32px; height: 32px; border-radius: 11px; background: #0866FF; display: flex; align-items: center; justify-content: center;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round">
<circle cx="11" cy="11" r="6.5"></circle>
<path d="M19.5 19.5L16 16"></path>
</svg>
</div>
<span style="font-size: 1.25rem; font-weight: 700; letter-spacing: -0.035em;">서린즈</span>
</div>
<nav style="display: flex; align-items: center; gap: 1.5rem; font-size: 0.9375rem;">
<span style="font-weight: 700;"></span>
<span style="font-weight: 500; color: #5B6169;">랭킹</span>
<span style="font-weight: 500; color: #5B6169;">커뮤니티</span>
<span style="font-weight: 500; color: #5B6169;">스트리밍</span>
<span style="font-weight: 500; color: #5B6169;">소식</span>
<span style="font-weight: 500; color: #5B6169;">이벤트</span>
</nav>
<div style="flex-grow: 1; display: flex; justify-content: center;">
<div style="display: flex; align-items: center; gap: 0.625rem; width: 100%; max-width: 340px; height: 44px; padding: 0 1.125rem; background: #F5F6F8; border-radius: 999px;">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#9BA1A9" stroke-width="2.2" stroke-linecap="round">
<circle cx="11" cy="11" r="6.5"></circle>
<path d="M19.5 19.5L16 16"></path>
</svg>
<span style="font-size: 0.875rem; color: #9BA1A9;">닉네임 검색</span>
</div>
</div>
<button style="height: 42px; padding: 0 1.25rem; border: none; border-radius: 999px; background: #F2F4F7; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">로그인</button>
</header>
<!-- 3단 본문 -->
<div style="display: grid; grid-template-columns: 300px minmax(0, 1fr) 320px; gap: 2rem; padding: 32px 40px 56px;">
<!-- 좌: 게임 랭킹 + 검색 랭킹 -->
<aside>
<div style="display: flex; align-items: center;">
<h2 class="p-h2">게임 랭킹</h2>
<div style="flex-grow: 1;"></div>
<span class="p-more">전체 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.875rem;">
<span class="p-tab p-tab-on">승률</span>
<span class="p-tab">K/D</span>
<span class="p-tab">헤드샷</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 34px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="p-th">#</span>
<span class="p-th">유저</span>
<span class="p-th" style="text-align: right;">승률</span>
<span class="p-th" style="text-align: right;">K/D</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #DDE4EE; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">78.4%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">2.14</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #E5E0F0; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">74.9%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">1.96</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #E7E3D8; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽두시</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">71.2%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">1.88</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #D8E6E2; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대장</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">69.5%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">1.74</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #EDDFE2; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">한밤의저격수</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">68.1%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">1.71</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #E4E8ED; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">조용한총잡이</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">66.7%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">1.68</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #EAE6E0; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">야간부대장</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">65.2%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">1.63</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 56px 44px; align-items: center; gap: 0.625rem; height: 56px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;">
<span style="width: 32px; height: 32px; border-radius: 999px; background: #E2E9E6; flex-shrink: 0;"></span>
<span class="p-clip" style="font-size: 0.9375rem; font-weight: 600;">헤드샷장인</span>
</div>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">64.8%</span>
<span style="text-align: right; font-size: 0.875rem; color: #5B6169;">1.59</span>
</div>
<button style="width: 100%; height: 44px; margin-top: 1rem; border: none; border-radius: 999px; background: #F5F6F8; font-family: inherit; font-size: 0.875rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">랭킹 100위까지 보기</button>
<div style="height: 1px; background: #EDEFF2; margin: 2rem 0;"></div>
<div style="display: flex; align-items: center;">
<h2 class="p-h2">검색 랭킹</h2>
<div style="flex-grow: 1;"></div>
<span style="font-size: 0.8125rem; color: #9BA1A9;">18:20 기준</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 34px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="p-th">#</span>
<span class="p-th">닉네임</span>
<span class="p-th" style="text-align: right;">변동</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<span class="p-clip" style="font-size: 0.9375rem;">달빛조각사</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
<svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg>
<span style="font-size: 0.8125rem; color: #12805C;">2</span>
</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<span class="p-clip" style="font-size: 0.9375rem;">칼든햄스터</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
<svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg>
<span style="font-size: 0.8125rem; color: #C4453D;">1</span>
</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<span class="p-clip" style="font-size: 0.9375rem;">새벽두시</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
<svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg>
<span style="font-size: 0.8125rem; color: #12805C;">4</span>
</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<span class="p-clip" style="font-size: 0.9375rem;">무명연대장</span>
<span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<span class="p-clip" style="font-size: 0.9375rem;">한밤의저격수</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
<svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg>
<span style="font-size: 0.8125rem; color: #12805C;">3</span>
</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<span class="p-clip" style="font-size: 0.9375rem;">조용한총잡이</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
<svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg>
<span style="font-size: 0.8125rem; color: #C4453D;">2</span>
</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<span class="p-clip" style="font-size: 0.9375rem;">야간부대장</span>
<span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<span class="p-clip" style="font-size: 0.9375rem;">헤드샷장인</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
<svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg>
<span style="font-size: 0.8125rem; color: #12805C;">5</span>
</span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span>
<span class="p-clip" style="font-size: 0.9375rem;">삼보급단골</span>
<span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 20px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">10</span>
<span class="p-clip" style="font-size: 0.9375rem;">웨어하우스지박령</span>
<span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
<svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg>
<span style="font-size: 0.8125rem; color: #12805C;">1</span>
</span>
</div>
</aside>
<!-- 중앙: 검색 + 소식 + 커뮤니티 피드 -->
<main>
<div style="display: flex; align-items: center; gap: 0.875rem; height: 64px; padding: 0 0.5rem 0 1.25rem; background: #FFFFFF; border: 1px solid #E4E8ED; border-radius: 999px; box-shadow: 0 1px 2px rgba(20, 22, 26, 0.04), 0 10px 28px rgba(20, 22, 26, 0.06);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#9BA1A9" stroke-width="2.2" stroke-linecap="round">
<circle cx="11" cy="11" r="6.5"></circle>
<path d="M19.5 19.5L16 16"></path>
</svg>
<span style="flex-grow: 1; font-size: 1rem; color: #9BA1A9;">궁금한 닉네임을 입력해 보세요</span>
<button style="height: 48px; padding: 0 1.5rem; border: none; border-radius: 999px; background: #0866FF; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #FFFFFF; letter-spacing: -0.02em; cursor: pointer;">찾아보기</button>
</div>
<!-- 소식 2단 -->
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem; margin-top: 1.75rem;">
<div>
<div style="display: flex; align-items: center;">
<h2 class="p-h2">서든어택 소식</h2>
<div style="flex-grow: 1;"></div>
<span class="p-more">더 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.875rem;">
<span class="p-tab p-tab-on">공지사항</span>
<span class="p-tab">업데이트</span>
</div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">9월 정기점검 안내</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">09.02</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">비매너 이용자 제재 결과</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">가을 시즌 랭크전 일정</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">08.29</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">신규 맵 컨테이너 야드 추가</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">08.28</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">AK-47 반동 수치 조정</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">08.26</span>
</div>
</div>
</div>
<div>
<div style="display: flex; align-items: center;">
<h2 class="p-h2">서린즈 소식</h2>
<div style="flex-grow: 1;"></div>
<span class="p-more">더 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.875rem;">
<span class="p-tab p-tab-on">공지사항</span>
<span class="p-tab">업데이트</span>
</div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">맵별 승률 통계를 열었어요</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">09.03</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">전적 갱신이 두 배 빨라졌어요</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">즐겨찾기 알림 기능 추가</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">08.30</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">8월 서버 점검 결과 보고</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">08.27</span>
</div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="width: 6px; height: 6px; flex-shrink: 0;"></span>
<span class="p-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">커뮤니티 이용 규칙 정리</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">08.24</span>
</div>
</div>
</div>
</div>
<!-- 커뮤니티 -->
<div style="display: flex; align-items: center; margin-top: 2.25rem;">
<h2 class="p-h2">커뮤니티</h2>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-left: 1.125rem;">
<span class="p-tab p-tab-on">인기</span>
<span class="p-tab">최신</span>
<span class="p-tab">클립</span>
<span class="p-tab">공략</span>
</div>
<div style="flex-grow: 1;"></div>
<button style="height: 34px; padding: 0 1rem; border: none; border-radius: 999px; background: #F5F6F8; font-family: inherit; font-size: 0.8125rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">글쓰기</button>
</div>
<article style="padding: 1.5rem 0 1.75rem; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span style="width: 44px; height: 44px; border-radius: 999px; background: #DDE4EE;"></span>
<div style="flex-grow: 1; min-width: 0;">
<div style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</div>
<div style="font-size: 0.8125rem; color: #9BA1A9; margin-top: 0.0625rem;">병장 3호봉 · 12분 전</div>
</div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#9BA1A9"><circle cx="5" cy="12" r="1.6"></circle><circle cx="12" cy="12" r="1.6"></circle><circle cx="19" cy="12" r="1.6"></circle></svg>
</div>
<div style="aspect-ratio: 16 / 9; margin-top: 1rem; border-radius: 18px; background: #E4E8ED;"></div>
<div style="display: flex; align-items: center; gap: 1rem; margin-top: 0.875rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20.5S3.5 15.2 3.5 9.6A4.8 4.8 0 0112 6.6a4.8 4.8 0 018.5 3c0 5.6-8.5 10.9-8.5 10.9z"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 01-9 8.4 9.3 9.3 0 01-3.4-.6L3 21l1.8-5A8.1 8.1 0 013.5 11.5a8.4 8.4 0 019-8.4 8.4 8.4 0 018.5 8.4z"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 3.5L11 14"></path><path d="M21.5 3.5l-6.7 18-3.8-7.5L3.5 10.2z"></path></svg>
<div style="flex-grow: 1;"></div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5h12v17l-6-4.3-6 4.3z"></path></svg>
</div>
<div style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">좋아요 312개</div>
<p style="margin: 0.5rem 0 0; font-size: 0.9375rem; line-height: 1.65;">
<span style="font-weight: 600;">달빛조각사</span>
제3보급창고 마지막 라운드 1대4 역전했어요. 각도만 잘 잡으면 아직 됩니다.
</p>
<div style="font-size: 0.875rem; color: #9BA1A9; margin-top: 0.5rem;">댓글 24개 모두 보기</div>
</article>
<!-- 사진 없는 글 — 이미지 영역 없이 본문이 바로 온다 -->
<article style="padding: 1.5rem 0 1.75rem; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span style="width: 44px; height: 44px; border-radius: 999px; background: #E5E0F0;"></span>
<div style="flex-grow: 1; min-width: 0;">
<div style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</div>
<div style="font-size: 0.8125rem; color: #9BA1A9; margin-top: 0.0625rem;">소위 1호봉 · 48분 전</div>
</div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#9BA1A9"><circle cx="5" cy="12" r="1.6"></circle><circle cx="12" cy="12" r="1.6"></circle><circle cx="19" cy="12" r="1.6"></circle></svg>
</div>
<p style="margin: 1rem 0 0; font-size: 1.0625rem; line-height: 1.75;">웨어하우스 스나 자리 세 군데만 외우면 됩니다. 컨테이너 위, 이층 창문, 그리고 리스폰 바로 옆 기둥. 나머지는 다 걸립니다.</p>
<div style="display: flex; align-items: center; gap: 1rem; margin-top: 1.125rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20.5S3.5 15.2 3.5 9.6A4.8 4.8 0 0112 6.6a4.8 4.8 0 018.5 3c0 5.6-8.5 10.9-8.5 10.9z"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 01-9 8.4 9.3 9.3 0 01-3.4-.6L3 21l1.8-5A8.1 8.1 0 013.5 11.5a8.4 8.4 0 019-8.4 8.4 8.4 0 018.5 8.4z"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 3.5L11 14"></path><path d="M21.5 3.5l-6.7 18-3.8-7.5L3.5 10.2z"></path></svg>
<div style="flex-grow: 1;"></div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5h12v17l-6-4.3-6 4.3z"></path></svg>
</div>
<div style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">좋아요 274개</div>
<div style="font-size: 0.875rem; color: #9BA1A9; margin-top: 0.375rem;">댓글 18개 모두 보기</div>
</article>
<!-- 클립(영상) 글 -->
<article style="padding: 1.5rem 0 1.75rem; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span style="width: 44px; height: 44px; border-radius: 999px; background: #E7E3D8;"></span>
<div style="flex-grow: 1; min-width: 0;">
<div style="font-size: 0.9375rem; font-weight: 600;">새벽두시</div>
<div style="font-size: 0.8125rem; color: #9BA1A9; margin-top: 0.0625rem;">병장 1호봉 · 2시간 전</div>
</div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#9BA1A9"><circle cx="5" cy="12" r="1.6"></circle><circle cx="12" cy="12" r="1.6"></circle><circle cx="19" cy="12" r="1.6"></circle></svg>
</div>
<div style="position: relative; aspect-ratio: 16 / 9; margin-top: 1rem; border-radius: 18px; background: #EAE6E0; overflow: hidden;">
<div style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: flex; align-items: center; justify-content: center; width: 60px; height: 60px; border-radius: 999px; background: rgba(20, 22, 26, 0.55);">
<svg width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M8 5.5l11 6.5-11 6.5z"></path></svg>
</div>
<div style="position: absolute; right: 12px; bottom: 12px; padding: 0.25rem 0.5rem; border-radius: 8px; background: rgba(20, 22, 26, 0.6); font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">0:24</div>
</div>
<div style="display: flex; align-items: center; gap: 1rem; margin-top: 0.875rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20.5S3.5 15.2 3.5 9.6A4.8 4.8 0 0112 6.6a4.8 4.8 0 018.5 3c0 5.6-8.5 10.9-8.5 10.9z"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 01-9 8.4 9.3 9.3 0 01-3.4-.6L3 21l1.8-5A8.1 8.1 0 013.5 11.5a8.4 8.4 0 019-8.4 8.4 8.4 0 018.5 8.4z"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 3.5L11 14"></path><path d="M21.5 3.5l-6.7 18-3.8-7.5L3.5 10.2z"></path></svg>
<div style="flex-grow: 1;"></div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5h12v17l-6-4.3-6 4.3z"></path></svg>
</div>
<div style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">좋아요 198개</div>
<p style="margin: 0.5rem 0 0; font-size: 0.9375rem; line-height: 1.65;">
<span style="font-weight: 600;">새벽두시</span>
크로스카운터 1대3 클러치. 소리만 듣고 돌았습니다.
</p>
<div style="font-size: 0.875rem; color: #9BA1A9; margin-top: 0.5rem;">댓글 31개 모두 보기</div>
</article>
<button style="width: 100%; height: 48px; margin-top: 1.5rem; border: none; border-radius: 999px; background: #F5F6F8; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">게시글 더 보기</button>
</main>
<!-- 우: 스트리밍 + 이벤트 -->
<aside>
<div style="display: flex; align-items: center;">
<h2 class="p-h2">지금 방송 중</h2>
<div style="flex-grow: 1;"></div>
<span class="p-more">전체 보기</span>
</div>
<div style="margin-top: 1.125rem;">
<div style="aspect-ratio: 16 / 9; border-radius: 16px; background: #E4E8ED;"></div>
<div style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">삼보급 스나 연습</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.3125rem;">
<span style="width: 6px; height: 6px; border-radius: 999px; background: #E8443A;"></span>
<span style="font-size: 0.75rem; font-weight: 600; color: #E8443A;">LIVE</span>
<span style="font-size: 0.8125rem; color: #9BA1A9;">달빛조각사 · 1,204명</span>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1.25rem;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 88px; height: 52px; border-radius: 12px; background: #EAE6E0; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;">
<div class="p-clip" style="font-size: 0.875rem; font-weight: 600;">클랜전 리그 중계</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;">
<span style="width: 5px; height: 5px; border-radius: 999px; background: #E8443A;"></span>
<span style="font-size: 0.75rem; color: #9BA1A9;">무명연대 · 862명</span>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 88px; height: 52px; border-radius: 12px; background: #E2E9E6; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;">
<div class="p-clip" style="font-size: 0.875rem; font-weight: 600;">초보 탈출 폭파 강의</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;">
<span style="width: 5px; height: 5px; border-radius: 999px; background: #E8443A;"></span>
<span style="font-size: 0.75rem; color: #9BA1A9;">새벽두시 · 517명</span>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 88px; height: 52px; border-radius: 12px; background: #E8E4EC; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;">
<div class="p-clip" style="font-size: 0.875rem; font-weight: 600;">시청자 참여 한 판</div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;">
<span style="width: 5px; height: 5px; border-radius: 999px; background: #E8443A;"></span>
<span style="font-size: 0.75rem; color: #9BA1A9;">한밤의저격수 · 341명</span>
</div>
</div>
</div>
</div>
<div style="height: 1px; background: #EDEFF2; margin: 2rem 0;"></div>
<div style="display: flex; align-items: center;">
<h2 class="p-h2">이벤트</h2>
<div style="flex-grow: 1;"></div>
<span class="p-more">전체 보기</span>
</div>
<!-- 이벤트 슬라이드 — 가로형 배너(720:248)를 그대로 쓰고 설명은 아래에 -->
<div style="margin-top: 1.125rem;">
<div style="border: 1px solid #EDEFF2; border-radius: 14px; overflow: hidden;">
<img src="20260903074208.jpg" alt="서든 페스티벌 같이 갈래?" style="display: block; width: 100%; aspect-ratio: 720 / 248; object-fit: cover;">
</div>
<div style="font-size: 0.75rem; font-weight: 600; color: #9BA1A9; margin-top: 0.875rem;">시즌 이벤트</div>
<div class="p-clip" style="font-size: 1rem; font-weight: 700; letter-spacing: -0.03em; margin-top: 0.25rem;">서든 페스티벌 같이 갈래?</div>
<div style="font-size: 0.8125rem; color: #9BA1A9; margin-top: 0.375rem;">9월 1일 ~ 9월 30일</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.875rem;">
<span style="width: 20px; height: 4px; border-radius: 999px; background: #14161A;"></span>
<span style="width: 4px; height: 4px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 4px; height: 4px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 4px; height: 4px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 4px; height: 4px; border-radius: 999px; background: #D6DAE0;"></span>
<div style="flex-grow: 1;"></div>
<span style="font-size: 0.75rem; color: #9BA1A9; margin-right: 0.125rem;">1 / 5</span>
<span class="p-arw"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5l-7 7 7 7"></path></svg></span>
<span class="p-arw"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5l7 7-7 7"></path></svg></span>
</div>
</div>
</aside>
</div>
<!-- 푸터 -->
<footer style="display: flex; align-items: center; gap: 1.5rem; padding: 32px 40px 48px; border-top: 1px solid #EDEFF2; font-size: 0.875rem; color: #9BA1A9;">
<span style="font-weight: 600; color: #5B6169;">서린즈</span>
<span>이용약관</span>
<span>개인정보 처리방침</span>
<span>문의하기</span>
<div style="flex-grow: 1;"></div>
<span>넥슨 공개 API 기반 · 서든어택 비공식 서비스</span>
</footer>
</div>
</x-dc>
</body>
</html>
+305
View File
@@ -0,0 +1,305 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
/*PRETENDARD*/
/* 안 C — 앱 사이드바형: 좌측 고정 내비 + 본문 2단 */
body { margin: 0; }
.c-root, .c-root * { box-sizing: border-box; }
.c-root {
font-family: "Pretendard", -apple-system, "Apple SD Gothic Neo", sans-serif;
-webkit-font-smoothing: antialiased;
font-variant-numeric: tabular-nums;
letter-spacing: -0.015em;
}
a { color: #0866FF; text-decoration: none; }
a:hover { color: #0450CC; }
.c-h2 { margin: 0; font-size: 1.125rem; font-weight: 700; letter-spacing: -0.035em; }
.c-th { font-size: 0.75rem; font-weight: 500; color: #9BA1A9; }
.c-tab { display: inline-flex; align-items: center; height: 30px; padding: 0 0.8125rem; border-radius: 999px; background: #F5F6F8; font-size: 0.8125rem; font-weight: 500; color: #5B6169; }
.c-tab-on { background: #14161A; font-weight: 600; color: #FFFFFF; }
.c-more { font-size: 0.8125rem; font-weight: 600; color: #5B6169; }
.c-clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.c-arw { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border: 1px solid #E4E8ED; border-radius: 999px; background: #FFFFFF; cursor: pointer; }
.c-nav { display: flex; align-items: center; gap: 0.875rem; height: 46px; padding: 0 0.875rem; border-radius: 14px; font-size: 0.9375rem; font-weight: 500; color: #14161A; }
.c-nav-on { background: #EFF1F4; font-weight: 700; }
</style>
</helmet>
<div class="c-root" style="display: flex; width: 1440px; min-height: 2120px; background: #FFFFFF; color: #14161A;">
<!-- 좌측 고정 내비 -->
<aside style="display: flex; flex-direction: column; width: 240px; flex-shrink: 0; padding: 22px 14px 28px; background: #FAFBFC; border-right: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.625rem; padding: 0 0.75rem; margin-bottom: 1.5rem;">
<div style="width: 30px; height: 30px; border-radius: 10px; background: #0866FF; display: flex; align-items: center; justify-content: center;">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"></circle><path d="M19.5 19.5L16 16"></path></svg>
</div>
<span style="font-size: 1.1875rem; font-weight: 700; letter-spacing: -0.035em;">서린즈</span>
</div>
<div class="c-nav c-nav-on">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11.5L12 4.5l8 7"></path><path d="M6 10.5V20h12v-9.5"></path></svg>
</div>
<div class="c-nav">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"></circle><path d="M19.5 19.5L16 16"></path></svg>
전적 검색
</div>
<div class="c-nav">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V11M10 19V5M16 19v-6M21 19H3"></path></svg>
랭킹
</div>
<div class="c-nav">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="4"></rect><path d="M8 10h8M8 14h5"></path></svg>
커뮤니티
</div>
<div class="c-nav">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="5" width="19" height="13" rx="3.5"></rect><path d="M10.5 9.5l4.5 2.5-4.5 2.5z"></path></svg>
스트리밍
</div>
<div class="c-nav">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5h9l3.5 3.5v13H6z"></path><path d="M9 11h7M9 15h5"></path></svg>
소식
</div>
<div class="c-nav">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 9.5h17M3.5 9.5A2 2 0 015.5 7.5h13a2 2 0 012 2v9a2 2 0 01-2 2h-13a2 2 0 01-2-2z"></path><path d="M8 4.5v4M16 4.5v4"></path></svg>
이벤트
</div>
<div style="flex-grow: 1;"></div>
<div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 0.875rem; border-radius: 14px; background: #FFFFFF; border: 1px solid #EDEFF2;">
<span style="width: 36px; height: 36px; border-radius: 999px; background: #DDE4EE; flex-shrink: 0;"></span>
<div style="min-width: 0;">
<div class="c-clip" style="font-size: 0.875rem; font-weight: 600;">달빛조각사</div>
<div style="font-size: 0.75rem; color: #9BA1A9; margin-top: 0.0625rem;">병장 3호봉</div>
</div>
</div>
</aside>
<!-- 본문 -->
<div style="flex-grow: 1; min-width: 0;">
<header style="display: flex; align-items: center; gap: 1rem; height: 72px; padding: 0 40px; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.75rem; width: 100%; max-width: 460px; height: 46px; padding: 0 1.125rem; background: #F5F6F8; border-radius: 999px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9BA1A9" stroke-width="2.2" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"></circle><path d="M19.5 19.5L16 16"></path></svg>
<span style="font-size: 0.9375rem; color: #9BA1A9;">닉네임을 입력해 보세요</span>
</div>
<div style="flex-grow: 1;"></div>
<span style="display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border-radius: 999px; background: #F5F6F8;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 106 8c0 7-3 9-3 9h18s-3-2-3-9z"></path><path d="M13.7 21a2 2 0 01-3.4 0"></path></svg>
</span>
<span style="width: 42px; height: 42px; border-radius: 999px; background: #DDE4EE;"></span>
</header>
<div style="padding: 32px 40px 56px;">
<!-- 이벤트 슬라이드 -->
<div style="display: flex; align-items: center; margin-bottom: 1rem;">
<h2 class="c-h2">이벤트</h2><div style="flex-grow: 1;"></div><span class="c-more">전체 보기</span>
</div>
<!-- 가로형 배너(720:248) 그대로 + 아래 한 줄 설명 -->
<div style="border: 1px solid #EDEFF2; border-radius: 24px; overflow: hidden;">
<img src="20260903074208.jpg" alt="서든 페스티벌 같이 갈래?" style="display: block; width: 100%; aspect-ratio: 720 / 248; object-fit: cover;">
</div>
<div style="display: flex; align-items: center; gap: 0.875rem; height: 58px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">시즌 이벤트</span>
<span style="width: 1px; height: 12px; background: #E4E8ED;"></span>
<span style="font-size: 1rem; font-weight: 600;">서든 페스티벌 같이 갈래?</span>
<span style="font-size: 0.875rem; color: #9BA1A9;">9월 1일 ~ 9월 30일</span>
<div style="flex-grow: 1;"></div>
<span style="width: 22px; height: 5px; border-radius: 999px; background: #14161A;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="font-size: 0.8125rem; color: #9BA1A9; margin: 0 0.25rem 0 0.5rem;">1 / 5</span>
<span class="c-arw"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5l-7 7 7 7"></path></svg></span>
<span class="c-arw"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5l7 7-7 7"></path></svg></span>
</div>
<!-- 2단 -->
<div style="display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 2.5rem; margin-top: 2.5rem;">
<div>
<!-- 게임 랭킹 -->
<div style="display: flex; align-items: center;">
<h2 class="c-h2">게임 랭킹</h2>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-left: 1rem;">
<span class="c-tab c-tab-on">전체</span><span class="c-tab">폭파미션</span><span class="c-tab">팀 데스매치</span>
</div>
<div style="flex-grow: 1;"></div><span class="c-more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: 36px minmax(0, 1fr) 80px 68px 76px; align-items: center; gap: 0.75rem; height: 36px; margin-top: 1rem; border-bottom: 1px solid #E4E8ED;">
<span class="c-th">순위</span><span class="c-th">유저</span>
<span class="c-th" style="text-align: right;">승률</span>
<span class="c-th" style="text-align: right;">K/D</span>
<span class="c-th" style="text-align: right;">판수</span>
</div>
<div style="display: grid; grid-template-columns: 36px minmax(0, 1fr) 80px 68px 76px; align-items: center; gap: 0.75rem; height: 54px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 700;">1</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;"><span style="width: 32px; height: 32px; border-radius: 999px; background: #DDE4EE; flex-shrink: 0;"></span><span class="c-clip" style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</span><span style="font-size: 0.8125rem; color: #9BA1A9; flex-shrink: 0;">병장 3호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">78.4%</span>
<span style="text-align: right; font-size: 0.9375rem;">2.14</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">1,284</span>
</div>
<div style="display: grid; grid-template-columns: 36px minmax(0, 1fr) 80px 68px 76px; align-items: center; gap: 0.75rem; height: 54px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 700;">2</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;"><span style="width: 32px; height: 32px; border-radius: 999px; background: #E5E0F0; flex-shrink: 0;"></span><span class="c-clip" style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</span><span style="font-size: 0.8125rem; color: #9BA1A9; flex-shrink: 0;">소위 1호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">74.9%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.96</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">2,041</span>
</div>
<div style="display: grid; grid-template-columns: 36px minmax(0, 1fr) 80px 68px 76px; align-items: center; gap: 0.75rem; height: 54px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 700;">3</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;"><span style="width: 32px; height: 32px; border-radius: 999px; background: #E7E3D8; flex-shrink: 0;"></span><span class="c-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽두시</span><span style="font-size: 0.8125rem; color: #9BA1A9; flex-shrink: 0;">병장 1호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">71.2%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.88</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">986</span>
</div>
<div style="display: grid; grid-template-columns: 36px minmax(0, 1fr) 80px 68px 76px; align-items: center; gap: 0.75rem; height: 54px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 600; color: #9BA1A9;">4</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;"><span style="width: 32px; height: 32px; border-radius: 999px; background: #D8E6E2; flex-shrink: 0;"></span><span class="c-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대장</span><span style="font-size: 0.8125rem; color: #9BA1A9; flex-shrink: 0;">중사 2호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">69.5%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.74</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">3,102</span>
</div>
<div style="display: grid; grid-template-columns: 36px minmax(0, 1fr) 80px 68px 76px; align-items: center; gap: 0.75rem; height: 54px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 600; color: #9BA1A9;">5</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;"><span style="width: 32px; height: 32px; border-radius: 999px; background: #EDDFE2; flex-shrink: 0;"></span><span class="c-clip" style="font-size: 0.9375rem; font-weight: 600;">한밤의저격수</span><span style="font-size: 0.8125rem; color: #9BA1A9; flex-shrink: 0;">상사 1호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">68.1%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.71</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">742</span>
</div>
<div style="display: grid; grid-template-columns: 36px minmax(0, 1fr) 80px 68px 76px; align-items: center; gap: 0.75rem; height: 54px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.9375rem; font-weight: 600; color: #9BA1A9;">6</span>
<div style="display: flex; align-items: center; gap: 0.625rem; min-width: 0;"><span style="width: 32px; height: 32px; border-radius: 999px; background: #E4E8ED; flex-shrink: 0;"></span><span class="c-clip" style="font-size: 0.9375rem; font-weight: 600;">조용한총잡이</span><span style="font-size: 0.8125rem; color: #9BA1A9; flex-shrink: 0;">병장 2호봉</span></div>
<span style="text-align: right; font-size: 0.9375rem; font-weight: 600;">66.7%</span>
<span style="text-align: right; font-size: 0.9375rem;">1.68</span>
<span style="text-align: right; font-size: 0.9375rem; color: #5B6169;">1,530</span>
</div>
<button style="width: 100%; height: 46px; margin-top: 1rem; border: none; border-radius: 999px; background: #F5F6F8; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">랭킹 100위까지 보기</button>
<!-- 소식 2단 -->
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2rem; margin-top: 2.5rem;">
<div>
<div style="display: flex; align-items: center;"><h2 class="c-h2">서든어택 소식</h2><div style="flex-grow: 1;"></div><span class="c-more">더 보기</span></div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.875rem;"><span class="c-tab c-tab-on">공지사항</span><span class="c-tab">업데이트</span></div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">9월 정기점검 안내</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.02</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">비매너 이용자 제재 결과</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">가을 시즌 랭크전 일정</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.29</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">신규 맵 컨테이너 야드 추가</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.28</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">AK-47 반동 수치 조정</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.26</span></div>
</div>
</div>
<div>
<div style="display: flex; align-items: center;"><h2 class="c-h2">서린즈 소식</h2><div style="flex-grow: 1;"></div><span class="c-more">더 보기</span></div>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.875rem;"><span class="c-tab c-tab-on">공지사항</span><span class="c-tab">업데이트</span></div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">맵별 승률 통계를 열었어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.03</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">전적 갱신이 두 배 빨라졌어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">즐겨찾기 알림 기능 추가</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.30</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">8월 서버 점검 결과 보고</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.27</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">커뮤니티 이용 규칙 정리</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.24</span></div>
</div>
</div>
</div>
<!-- 커뮤니티 -->
<div style="display: flex; align-items: center; margin-top: 2.5rem;">
<h2 class="c-h2">커뮤니티</h2>
<div style="display: flex; align-items: center; gap: 0.375rem; margin-left: 1rem;">
<span class="c-tab c-tab-on">인기</span><span class="c-tab">최신</span><span class="c-tab">클립</span>
</div>
<div style="flex-grow: 1;"></div><span class="c-more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.75rem; margin-top: 1.125rem;">
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #E4E8ED; overflow: hidden;">
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.4375rem; height: 44px; padding: 0 0.75rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 20px; height: 20px; border-radius: 999px; background: #C9D2DD;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">달빛조각사</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.3A4.2 4.2 0 0112 8a4.2 4.2 0 017 2.7c0 4.9-7 9.3-7 9.3z"></path></svg><span style="font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">312</span></div>
</div>
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #EAE6E0; overflow: hidden;">
<div style="position: absolute; top: 10px; right: 10px;"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="3" width="13" height="13" rx="3"></rect><path d="M16 19.5A2.5 2.5 0 0113.5 22H6a3 3 0 01-3-3V9.5"></path></svg></div>
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.4375rem; height: 44px; padding: 0 0.75rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 20px; height: 20px; border-radius: 999px; background: #D8D2C8;"></span><span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">칼든햄스터</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.3A4.2 4.2 0 0112 8a4.2 4.2 0 017 2.7c0 4.9-7 9.3-7 9.3z"></path></svg><span style="font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">274</span></div>
</div>
<!-- 사진 없는 글 — 본문이 곧 썸네일이 된다 -->
<div style="display: flex; flex-direction: column; aspect-ratio: 1 / 1; padding: 18px; border-radius: 18px; background: #F5F6F8;">
<p style="margin: 0; font-size: 0.9375rem; line-height: 1.6; font-weight: 500;">폭파 삼보급에서 A 롱각 잡을 때 다들 어디 서세요? 요즘 자꾸 먼저 죽습니다.</p>
<div style="flex-grow: 1;"></div>
<div style="display: flex; align-items: center; gap: 0.4375rem;">
<span style="width: 20px; height: 20px; border-radius: 999px; background: #DDE4EE;"></span>
<span class="c-clip" style="flex-grow: 1; min-width: 0; font-size: 0.75rem; font-weight: 600;">새벽두시</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#5B6169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-9.3A4.2 4.2 0 0112 8a4.2 4.2 0 017 2.7c0 4.9-7 9.3-7 9.3z"></path></svg>
<span style="font-size: 0.75rem; font-weight: 600; color: #5B6169;">198</span>
</div>
</div>
</div>
</div>
<!-- 우측 레일 -->
<aside>
<div style="display: flex; align-items: center;">
<h2 class="c-h2">검색 랭킹</h2><div style="flex-grow: 1;"></div><span style="font-size: 0.8125rem; color: #9BA1A9;">18:20 기준</span>
</div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 34px; margin-top: 1rem; border-bottom: 1px solid #E4E8ED;">
<span class="c-th">#</span><span class="c-th">닉네임</span><span class="c-th" style="text-align: right;">변동</span>
</div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 700;">1</span><span class="c-clip" style="font-size: 0.9375rem;">달빛조각사</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">2</span></span></div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 700;">2</span><span class="c-clip" style="font-size: 0.9375rem;">칼든햄스터</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.8125rem; color: #C4453D;">1</span></span></div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 700;">3</span><span class="c-clip" style="font-size: 0.9375rem;">새벽두시</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">4</span></span></div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span><span class="c-clip" style="font-size: 0.9375rem;">무명연대장</span><span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span></div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span><span class="c-clip" style="font-size: 0.9375rem;">한밤의저격수</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">3</span></span></div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span><span class="c-clip" style="font-size: 0.9375rem;">조용한총잡이</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.8125rem; color: #C4453D;">2</span></span></div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span><span class="c-clip" style="font-size: 0.9375rem;">야간부대장</span><span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span></div>
<div style="display: grid; grid-template-columns: 24px minmax(0, 1fr) 48px; align-items: center; gap: 0.625rem; height: 42px;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span><span class="c-clip" style="font-size: 0.9375rem;">헤드샷장인</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">5</span></span></div>
<div style="height: 1px; background: #EDEFF2; margin: 2rem 0;"></div>
<div style="display: flex; align-items: center;">
<h2 class="c-h2">지금 방송 중</h2><div style="flex-grow: 1;"></div><span class="c-more">전체 보기</span>
</div>
<div style="margin-top: 1.125rem;">
<div style="aspect-ratio: 16 / 9; border-radius: 16px; background: #E4E8ED;"></div>
<div class="c-clip" style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">삼보급 스나 연습</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.3125rem;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; font-weight: 600; color: #E8443A;">LIVE</span><span style="font-size: 0.8125rem; color: #9BA1A9;">달빛조각사 · 1,204명</span></div>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1.25rem;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 88px; height: 52px; border-radius: 12px; background: #EAE6E0; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;"><div class="c-clip" style="font-size: 0.875rem; font-weight: 600;">클랜전 리그 중계</div><div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;"><span style="width: 5px; height: 5px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; color: #9BA1A9;">무명연대 · 862명</span></div></div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 88px; height: 52px; border-radius: 12px; background: #E2E9E6; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;"><div class="c-clip" style="font-size: 0.875rem; font-weight: 600;">초보 탈출 폭파 강의</div><div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;"><span style="width: 5px; height: 5px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; color: #9BA1A9;">새벽두시 · 517명</span></div></div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 88px; height: 52px; border-radius: 12px; background: #E8E4EC; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;"><div class="c-clip" style="font-size: 0.875rem; font-weight: 600;">시청자 참여 한 판</div><div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;"><span style="width: 5px; height: 5px; border-radius: 999px; background: #E8443A;"></span><span style="font-size: 0.75rem; color: #9BA1A9;">한밤의저격수 · 341명</span></div></div>
</div>
</div>
</aside>
</div>
</div>
<footer style="display: flex; align-items: center; gap: 1.5rem; padding: 32px 40px 48px; border-top: 1px solid #EDEFF2; font-size: 0.875rem; color: #9BA1A9;">
<span style="font-weight: 600; color: #5B6169;">서린즈</span><span>이용약관</span><span>개인정보 처리방침</span><span>문의하기</span>
<div style="flex-grow: 1;"></div><span>넥슨 공개 API 기반 · 서든어택 비공식 서비스</span>
</footer>
</div>
</div>
</x-dc>
</body>
</html>
+478
View File
@@ -0,0 +1,478 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
/*PRETENDARD*/
/* 서린즈 홈 (확정) — 최상단 검색 히어로 + 균등 3열 밀도 그리드 */
body { margin: 0; }
.d-root, .d-root * { box-sizing: border-box; }
.d-root {
font-family: "Pretendard", -apple-system, "Apple SD Gothic Neo", sans-serif;
-webkit-font-smoothing: antialiased;
font-variant-numeric: tabular-nums;
letter-spacing: -0.015em;
}
a { color: #0866FF; text-decoration: none; }
a:hover { color: #0450CC; }
.d-h2 { margin: 0; font-size: 1.125rem; font-weight: 700; letter-spacing: -0.035em; }
.d-th { font-size: 0.75rem; font-weight: 500; color: #9BA1A9; }
.d-tab { display: inline-flex; align-items: center; height: 30px; padding: 0 0.8125rem; border-radius: 999px; background: #F5F6F8; font-size: 0.8125rem; font-weight: 500; color: #5B6169; }
.d-tab-on { background: #14161A; font-weight: 600; color: #FFFFFF; }
.d-more { font-size: 0.8125rem; font-weight: 600; color: #5B6169; }
.d-clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.d-badge { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 8px; background: #F0F2F5; }
.d-mark { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 8px; background: #EDEFF2; }
.d-mv { display: flex; align-items: center; justify-content: flex-end; gap: 0.1875rem; }
.d-arw { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border: 1px solid #E4E8ED; border-radius: 999px; background: #FFFFFF; cursor: pointer; }
</style>
</helmet>
<div class="d-root" style="width: 1440px; min-height: 2420px; background: #FFFFFF; color: #14161A;">
<header style="display: flex; align-items: center; gap: 1.75rem; height: 72px; padding: 0 40px; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; align-items: center; gap: 0.625rem;">
<div style="width: 32px; height: 32px; border-radius: 11px; background: #0866FF; display: flex; align-items: center; justify-content: center;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"></circle><path d="M19.5 19.5L16 16"></path></svg>
</div>
<span style="font-size: 1.25rem; font-weight: 700; letter-spacing: -0.035em;">서린즈</span>
</div>
<nav style="display: flex; align-items: center; gap: 1.5rem; margin-left: 0.75rem; font-size: 0.9375rem;">
<span style="font-weight: 700;"></span>
<span style="font-weight: 500; color: #5B6169;">전적 검색</span>
<span style="font-weight: 500; color: #5B6169;">랭킹</span>
<span style="font-weight: 500; color: #5B6169;">커뮤니티</span>
<span style="font-weight: 500; color: #5B6169;">스트리밍</span>
<span style="font-weight: 500; color: #5B6169;">소식</span>
<span style="font-weight: 500; color: #5B6169;">이벤트</span>
</nav>
<div style="flex-grow: 1;"></div>
<button style="height: 42px; padding: 0 1.25rem; border: none; border-radius: 999px; background: #F2F4F7; font-family: inherit; font-size: 0.9375rem; font-weight: 600; color: #14161A; letter-spacing: -0.015em; cursor: pointer;">로그인</button>
</header>
<!-- 검색 히어로 — 옅은 색면으로 아래 그리드와 층을 나눈다 -->
<section style="background: #FAFBFC; border-bottom: 1px solid #EDEFF2;">
<div style="display: flex; flex-direction: column; align-items: center; padding: 52px 40px 46px;">
<h1 style="margin: 0; text-align: center; font-size: 2.125rem; line-height: 1.28; font-weight: 700; letter-spacing: -0.045em;">누구의 전적이 궁금하세요?</h1>
<p style="margin: 0.75rem 0 0; text-align: center; font-size: 0.9375rem; line-height: 1.65; color: #5B6169;">닉네임만 알려주시면 최근 경기 흐름부터 자주 쓰는 무기까지 정리해 드릴게요.</p>
<div style="display: flex; align-items: center; gap: 0.875rem; width: 100%; max-width: 700px; height: 66px; margin-top: 1.625rem; padding: 0 0.5rem 0 1.375rem; background: #FFFFFF; border: 1px solid #E4E8ED; border-radius: 999px; box-shadow: 0 1px 2px rgba(20, 22, 26, 0.04), 0 12px 32px rgba(20, 22, 26, 0.07);">
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="#9BA1A9" stroke-width="2.2" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"></circle><path d="M19.5 19.5L16 16"></path></svg>
<span style="flex-grow: 1; font-size: 1.0625rem; color: #9BA1A9;">닉네임을 입력해 보세요</span>
<button style="height: 50px; padding: 0 1.75rem; border: none; border-radius: 999px; background: #0866FF; font-family: inherit; font-size: 1rem; font-weight: 600; color: #FFFFFF; letter-spacing: -0.02em; cursor: pointer;">찾아보기</button>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem; margin-top: 1.25rem;">
<span style="font-size: 0.875rem; color: #9BA1A9; margin-right: 0.25rem;">최근에 찾아봤어요</span>
<span style="display: inline-flex; align-items: center; gap: 0.5rem; height: 36px; padding: 0 0.875rem 0 0.375rem; border-radius: 999px; background: #FFFFFF; border: 1px solid #EDEFF2; font-size: 0.875rem; font-weight: 600;"><span style="width: 24px; height: 24px; border-radius: 999px; background: #DDE4EE;"></span>달빛조각사</span>
<span style="display: inline-flex; align-items: center; gap: 0.5rem; height: 36px; padding: 0 0.875rem 0 0.375rem; border-radius: 999px; background: #FFFFFF; border: 1px solid #EDEFF2; font-size: 0.875rem; font-weight: 600;"><span style="width: 24px; height: 24px; border-radius: 999px; background: #E5E0F0;"></span>칼든햄스터</span>
<span style="display: inline-flex; align-items: center; gap: 0.5rem; height: 36px; padding: 0 0.875rem 0 0.375rem; border-radius: 999px; background: #FFFFFF; border: 1px solid #EDEFF2; font-size: 0.875rem; font-weight: 600;"><span style="width: 24px; height: 24px; border-radius: 999px; background: #E7E3D8;"></span>새벽두시</span>
<span style="display: inline-flex; align-items: center; gap: 0.5rem; height: 36px; padding: 0 0.875rem 0 0.375rem; border-radius: 999px; background: #FFFFFF; border: 1px solid #EDEFF2; font-size: 0.875rem; font-weight: 600;"><span style="width: 24px; height: 24px; border-radius: 999px; background: #D8E6E2;"></span>무명연대장</span>
</div>
</div>
</section>
<!-- 균등 3열 그리드 -->
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2.5rem 2rem; padding: 36px 40px 56px;">
<!-- 이벤트 슬라이드 (2칸) -->
<section style="grid-column: span 2;">
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">이벤트</h2><div style="flex-grow: 1;"></div><span class="d-more">전체 보기</span>
</div>
<!-- 가로형 배너(720:248) 그대로 + 아래 한 줄 설명 -->
<div style="border: 1px solid #EDEFF2; border-radius: 24px; overflow: hidden;">
<img src="20260903074208.jpg" alt="서든 페스티벌 같이 갈래?" style="display: block; width: 100%; aspect-ratio: 720 / 248; object-fit: cover;">
</div>
<div style="display: flex; align-items: center; gap: 0.75rem; height: 56px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">시즌 이벤트</span>
<span style="width: 1px; height: 12px; background: #E4E8ED;"></span>
<span class="d-clip" style="font-size: 1rem; font-weight: 600;">서든 페스티벌 같이 갈래?</span>
<span style="font-size: 0.875rem; color: #9BA1A9; white-space: nowrap;">9월 1일 ~ 9월 30일</span>
<div style="flex-grow: 1;"></div>
<span style="width: 22px; height: 5px; border-radius: 999px; background: #14161A;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="width: 5px; height: 5px; border-radius: 999px; background: #D6DAE0;"></span>
<span style="font-size: 0.8125rem; color: #9BA1A9; margin: 0 0.25rem 0 0.5rem;">1 / 5</span>
<span class="d-arw"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5l-7 7 7 7"></path></svg></span>
<span class="d-arw"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5l7 7-7 7"></path></svg></span>
</div>
</section>
<!-- 검색 랭킹 -->
<section>
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">검색 랭킹</h2><div style="flex-grow: 1;"></div><span style="font-size: 0.8125rem; color: #9BA1A9;">18:20 기준</span>
</div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 34px; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">순위</span><span class="d-th">닉네임</span><span class="d-th" style="text-align: right;">변동</span>
</div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 700;">1</span><span class="d-clip" style="font-size: 0.9375rem;">달빛조각사</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">2</span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 700;">2</span><span class="d-clip" style="font-size: 0.9375rem;">칼든햄스터</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.8125rem; color: #C4453D;">1</span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 700;">3</span><span class="d-clip" style="font-size: 0.9375rem;">새벽두시</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">4</span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span><span class="d-clip" style="font-size: 0.9375rem;">무명연대장</span><span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span><span class="d-clip" style="font-size: 0.9375rem;">한밤의저격수</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">3</span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span><span class="d-clip" style="font-size: 0.9375rem;">조용한총잡이</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.8125rem; color: #C4453D;">2</span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span><span class="d-clip" style="font-size: 0.9375rem;">야간부대장</span><span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px; border-bottom: 1px solid #F3F5F7;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span><span class="d-clip" style="font-size: 0.9375rem;">헤드샷장인</span><span style="display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;"><svg width="10" height="10" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.8125rem; color: #12805C;">5</span></span></div>
<div style="display: grid; grid-template-columns: 26px minmax(0, 1fr) 52px; align-items: center; gap: 0.625rem; height: 42px;"><span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span><span class="d-clip" style="font-size: 0.9375rem;">삼보급단골</span><span style="display: flex; align-items: center; justify-content: flex-end;"><span style="width: 10px; height: 1px; background: #C9CDD3;"></span></span></div>
</section>
<!-- 게임 랭킹 (2칸) — 계급 / 랭크전 / 클랜 세 갈래를 나란히 -->
<section style="grid-column: span 2;">
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">게임 랭킹</h2>
<span style="margin-left: 0.75rem; font-size: 0.8125rem; color: #9BA1A9;">매일 오전 8시 갱신</span>
<div style="flex-grow: 1;"></div><span class="d-more">전체 보기</span>
</div>
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem;">
<!-- 계급 랭킹 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">계급 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
<span class="d-tab d-tab-on">통합</span><span class="d-tab">시즌</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 32px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">#</span><span></span><span class="d-th">유저</span>
<span class="d-th" style="text-align: right;">등락</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽두시</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">4</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대장</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">한밤의저격수</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">조용한총잡이</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">야간부대장</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">헤드샷장인</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">5</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급단골</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">10</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">웨어하우스지박령</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
</div>
<!-- 랭크전 랭킹 — RP 점수를 함께 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">랭크전 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
<span class="d-tab d-tab-on">솔로</span><span class="d-tab">파티</span><span class="d-tab">클랜</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 32px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">#</span><span></span><span class="d-th">유저</span>
<span class="d-th" style="text-align: right;">RP</span>
<span class="d-th" style="text-align: right;">등락</span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">헤드샷장인</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,486</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">달빛조각사</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,451</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급단골</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,398</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">6</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4l2.3 4.9 5.4.7-3.9 3.8 1 5.3L12 16.2 7.2 18.7l1-5.3L4.3 9.6l5.4-.7z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">칼든햄스터</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,344</span><span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">웨어하우스지박령</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,301</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽두시</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,276</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">조용한총잡이</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,240</span><span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">한밤의저격수</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,205</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">4</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span>
<span class="d-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10l7 4 7-4"></path><path d="M5 15l7 4 7-4"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">야간부대장</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,181</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 26px minmax(0, 1fr) 48px 38px; align-items: center; gap: 0.5rem; height: 46px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">10</span>
<span class="d-badge"><svg width="13" height="13" viewBox="0 0 24 24" fill="#7A828C"><path d="M12 4.5l5.5 7.5L12 19.5 6.5 12z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대장</span>
<span style="text-align: right; font-size: 0.875rem; font-weight: 600;">2,154</span><span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
</div>
<!-- 클랜 랭킹 — 클랜 마크 + 클랜명 -->
<div>
<div style="font-size: 0.9375rem; font-weight: 700;">클랜 랭킹</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-top: 0.625rem;">
<span class="d-tab d-tab-on">공식</span><span class="d-tab">일반</span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 32px; margin-top: 0.75rem; border-bottom: 1px solid #E4E8ED;">
<span class="d-th">#</span><span></span><span class="d-th">클랜</span>
<span class="d-th" style="text-align: right;">등락</span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">1</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">무명연대</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">2</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽클랜</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 700;">3</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">야간부대</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">4</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">정예사격단</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">3</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">5</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급수호대</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">6</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">헤드샷연구소</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">4</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">7</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">크로스카운터</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">2</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">8</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">웨어하우스단</span>
<span class="d-mv"><span style="width: 9px; height: 1px; background: #C9CDD3;"></span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px; border-bottom: 1px solid #F3F5F7;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">9</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 2.8v5.2c0 3.8-2.8 7.2-7 9-4.2-1.8-7-5.2-7-9V6.3z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">새벽정찰대</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#12805C"><path d="M6 2l4 7H2z"></path></svg><span style="font-size: 0.75rem; color: #12805C;">1</span></span>
</div>
<div style="display: grid; grid-template-columns: 18px 28px minmax(0, 1fr) 38px; align-items: center; gap: 0.5rem; height: 46px;">
<span style="font-size: 0.875rem; font-weight: 600; color: #9BA1A9;">10</span>
<span class="d-mark"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#7A828C" stroke-width="1.8" stroke-linejoin="round"><path d="M12 3.5l7 4v9l-7 4-7-4v-9z"></path></svg></span>
<span class="d-clip" style="font-size: 0.9375rem; font-weight: 600;">삼보급기동대</span>
<span class="d-mv"><svg width="9" height="9" viewBox="0 0 12 12" fill="#C4453D"><path d="M6 10L2 3h8z"></path></svg><span style="font-size: 0.75rem; color: #C4453D;">3</span></span>
</div>
</div>
</div>
<!-- 순위 구간 이동 — 세 랭킹이 같은 구간을 함께 본다 -->
<div style="display: flex; align-items: center; gap: 0.75rem; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid #EDEFF2;">
<span style="font-size: 0.8125rem; color: #9BA1A9;">1 ~ 10위 · 전체 100위</span>
<div style="flex-grow: 1;"></div>
<span class="d-arw" style="opacity: 0.4;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5l-7 7 7 7"></path></svg></span>
<div style="display: flex; align-items: center; gap: 0.125rem;">
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; background: #14161A; font-size: 0.875rem; font-weight: 700; color: #FFFFFF;">1</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">2</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">3</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">4</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">5</span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 26px; height: 34px; font-size: 0.875rem; color: #9BA1A9;"></span>
<span style="display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 34px; padding: 0 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 500; color: #5B6169;">10</span>
</div>
<span class="d-arw"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#14161A" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5l7 7-7 7"></path></svg></span>
</div>
</section>
<!-- 커뮤니티 -->
<section>
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">커뮤니티</h2><div style="flex-grow: 1;"></div><span class="d-more">전체 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem;"><span class="d-tab d-tab-on">인기</span><span class="d-tab">최신</span><span class="d-tab">클립</span></div>
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.75rem; margin-top: 0.875rem;">
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #E4E8ED; overflow: hidden;">
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.4375rem; height: 42px; padding: 0 0.75rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 20px; height: 20px; border-radius: 999px; background: #C9D2DD;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">달빛조각사</span><span style="font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">312</span></div>
</div>
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #EAE6E0; overflow: hidden;">
<div style="position: absolute; top: 10px; right: 10px;"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="3" width="13" height="13" rx="3"></rect><path d="M16 19.5A2.5 2.5 0 0113.5 22H6a3 3 0 01-3-3V9.5"></path></svg></div>
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.4375rem; height: 42px; padding: 0 0.75rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 20px; height: 20px; border-radius: 999px; background: #D8D2C8;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">칼든햄스터</span><span style="font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">274</span></div>
</div>
<!-- 사진 없는 글 -->
<div style="display: flex; flex-direction: column; aspect-ratio: 1 / 1; padding: 16px; border-radius: 18px; background: #F5F6F8;">
<p style="margin: 0; font-size: 0.875rem; line-height: 1.6; font-weight: 500;">폭파 삼보급 A 롱각, 다들 어디 서세요?</p>
<div style="flex-grow: 1;"></div>
<div style="display: flex; align-items: center; gap: 0.4375rem;">
<span style="width: 20px; height: 20px; border-radius: 999px; background: #DDE4EE;"></span>
<span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.75rem; font-weight: 600;">새벽두시</span>
<span style="font-size: 0.75rem; font-weight: 600; color: #5B6169;">198</span>
</div>
</div>
<!-- 클립(영상) 글 -->
<div style="position: relative; aspect-ratio: 1 / 1; border-radius: 18px; background: #E8E4EC; overflow: hidden;">
<div style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 999px; background: rgba(20, 22, 26, 0.55);">
<svg width="17" height="17" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M8 5.5l11 6.5-11 6.5z"></path></svg>
</div>
<div style="position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 0.4375rem; height: 42px; padding: 0 0.75rem; background: rgba(20, 22, 26, 0.5);"><span style="width: 20px; height: 20px; border-radius: 999px; background: #D5CEDC;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">무명연대장</span><span style="font-size: 0.75rem; font-weight: 600; color: #FFFFFF;">163</span></div>
</div>
</div>
</section>
<!-- 서든어택 소식 -->
<section>
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">서든어택 소식</h2><div style="flex-grow: 1;"></div><span class="d-more">더 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem;"><span class="d-tab d-tab-on">공지사항</span><span class="d-tab">업데이트</span></div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">9월 정기점검 안내</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.02</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">비매너 이용자 제재 결과</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">가을 시즌 랭크전 일정</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.29</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">신규 맵 컨테이너 야드 추가</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.28</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">AK-47 반동 수치 조정</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.26</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">클랜전 매칭 로직 개선</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.22</span></div>
</div>
</section>
<!-- 서린즈 소식 -->
<section>
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">서린즈 소식</h2><div style="flex-grow: 1;"></div><span class="d-more">더 보기</span>
</div>
<div style="display: flex; align-items: center; gap: 0.375rem;"><span class="d-tab d-tab-on">공지사항</span><span class="d-tab">업데이트</span></div>
<div style="margin-top: 0.75rem; border-top: 1px solid #E4E8ED;">
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">맵별 승률 통계를 열었어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.03</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; border-radius: 999px; background: #0866FF; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">전적 갱신이 두 배 빨라졌어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">09.01</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">즐겨찾기 알림 기능 추가</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.30</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">8월 서버 점검 결과 보고</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.27</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">커뮤니티 이용 규칙 정리</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.24</span></div>
<div style="display: flex; align-items: center; gap: 0.625rem; height: 46px; border-bottom: 1px solid #F3F5F7;"><span style="width: 6px; height: 6px; flex-shrink: 0;"></span><span class="d-clip" style="flex-grow: 1; min-width: 0; font-size: 0.9375rem;">모바일 화면을 다듬었어요</span><span style="font-size: 0.8125rem; color: #9BA1A9;">08.20</span></div>
</div>
</section>
<!-- 스트리밍 -->
<section>
<div style="display: flex; align-items: center; margin-bottom: 0.875rem;">
<h2 class="d-h2">지금 방송 중</h2><div style="flex-grow: 1;"></div><span class="d-more">전체 보기</span>
</div>
<div>
<div style="aspect-ratio: 16 / 9; border-radius: 16px; background: #E4E8ED;"></div>
<div class="d-clip" style="font-size: 0.9375rem; font-weight: 600; margin-top: 0.75rem;">삼보급 스나 연습</div>
<div style="display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.3125rem;"><span style="width: 20px; height: 20px; border-radius: 999px; background: #DDE4EE; flex-shrink: 0;"></span><span style="font-size: 0.8125rem; color: #9BA1A9;">달빛조각사 · 1,204명</span></div>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1.25rem;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 92px; height: 54px; border-radius: 12px; background: #EAE6E0; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;"><div class="d-clip" style="font-size: 0.875rem; font-weight: 600;">클랜전 정기 리그 중계</div><div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;"><span style="width: 18px; height: 18px; border-radius: 999px; background: #D8E6E2; flex-shrink: 0;"></span><span style="font-size: 0.75rem; color: #9BA1A9;">무명연대 · 862명</span></div></div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 92px; height: 54px; border-radius: 12px; background: #E2E9E6; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;"><div class="d-clip" style="font-size: 0.875rem; font-weight: 600;">초보 탈출 폭파미션 강의</div><div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;"><span style="width: 18px; height: 18px; border-radius: 999px; background: #E7E3D8; flex-shrink: 0;"></span><span style="font-size: 0.75rem; color: #9BA1A9;">새벽두시 · 517명</span></div></div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 92px; height: 54px; border-radius: 12px; background: #E8E4EC; flex-shrink: 0;"></div>
<div style="flex-grow: 1; min-width: 0;"><div class="d-clip" style="font-size: 0.875rem; font-weight: 600;">시청자 참여 한 판</div><div style="display: flex; align-items: center; gap: 0.375rem; margin-top: 0.25rem;"><span style="width: 18px; height: 18px; border-radius: 999px; background: #E5E0F0; flex-shrink: 0;"></span><span style="font-size: 0.75rem; color: #9BA1A9;">한밤의저격수 · 341명</span></div></div>
</div>
</div>
</section>
</div>
<footer style="display: flex; align-items: center; gap: 1.5rem; padding: 32px 40px 48px; border-top: 1px solid #EDEFF2; font-size: 0.875rem; color: #9BA1A9;">
<span style="font-weight: 600; color: #5B6169;">서린즈</span><span>이용약관</span><span>개인정보 처리방침</span><span>문의하기</span>
<div style="flex-grow: 1;"></div><span>넥슨 공개 API 기반 · 서든어택 비공식 서비스</span>
</footer>
</div>
</x-dc>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
# 에디터 공통 포맷 — 3스택·여러 에디터에서 들여쓰기/인코딩 통일
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
# Dart (dart format 기본 2칸)
[*.dart]
indent_size = 2
# Markdown — 줄 끝 공백이 의미를 가질 수 있어 트림 제외
[*.md]
trim_trailing_whitespace = false
# Makefile 은 탭 필수
[Makefile]
indent_style = tab
+39
View File
@@ -0,0 +1,39 @@
# ==========================================
# 프로젝트 전역 설정
# ==========================================
PROJECT_NAME=project
APP_ENV=development
BUILD_MODE=development
FRONTEND_DIR=frontend
BACKEND_DIR=backend
# ==========================================
# 포트 설정
# ==========================================
FRONTEND_PORT=5173
BACKEND_PORT=3000
WEBSOCKET_PORT=3001
DB_PORT=5432
REDIS_PORT=6379
# ==========================================
# Database 설정
# ==========================================
DB_IMAGE=postgres:15-alpine
DB_USER=dev_ttipo
DB_PASSWORD=dev_960426
DB_NAME=project_db
DB_DATA_DIR=./data/postgres
# ==========================================
# Redis 설정
# ==========================================
REDIS_IMAGE=redis:7-alpine
REDIS_PASSWORD=dev_960426
REDIS_DATA_DIR=./data/redis
# ==========================================
# API 경로 설정
# ==========================================
BACKEND_API_URL=http://localhost:3000/api
CORS_ORIGIN=http://localhost:5173
+39
View File
@@ -0,0 +1,39 @@
# ==========================================
# 프로젝트 전역 설정
# ==========================================
PROJECT_NAME=project
APP_ENV=production
BUILD_MODE=production
FRONTEND_DIR=frontend
BACKEND_DIR=backend
# ==========================================
# 포트 설정
# ==========================================
FRONTEND_PORT=80
BACKEND_PORT=3000
WEBSOCKET_PORT=3001
DB_PORT=5432
REDIS_PORT=6379
# ==========================================
# Database 설정
# ==========================================
DB_IMAGE=postgres:15-alpine
DB_USER=prd_ttipo
DB_PASSWORD=prd_960426
DB_NAME=project_db
DB_DATA_DIR=./data/postgres
# ==========================================
# Redis 설정
# ==========================================
REDIS_IMAGE=redis:7-alpine
REDIS_PASSWORD=prd_960426
REDIS_DATA_DIR=./data/redis
# ==========================================
# API 경로 설정
# ==========================================
BACKEND_API_URL=http://localhost:3000/api
CORS_ORIGIN=http://localhost:5173
+24
View File
@@ -0,0 +1,24 @@
# 줄바꿈 정규화 — 어떤 OS에서 클론해도 저장소 내부는 LF로 통일 (Windows 작업 시 CRLF 변환 churn 방지)
* text=auto eol=lf
# Windows 전용 스크립트는 CRLF 유지
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# 셸 스크립트는 LF 강제
*.sh text eol=lf
# 바이너리 — 변환/diff 제외
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.jar binary
*.keystore binary
+52
View File
@@ -0,0 +1,52 @@
# ==========================================
# 환경 변수 (민감정보) — 절대 커밋 금지
# ==========================================
.env
.env.*
!.env.example
!.env.*.example
# ==========================================
# 도커 영속 데이터
# ==========================================
data/
!data/.gitkeep
db-data/
redis-data/
# ==========================================
# 의존성/빌드 산출물
# ==========================================
node_modules/
dist/
build/
coverage/
.tsbuildinfo
*.tsbuildinfo
# ==========================================
# 로그
# ==========================================
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# ==========================================
# IDE / OS
# ==========================================
.idea/
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.DS_Store
Thumbs.db
# ==========================================
# 캐시
# ==========================================
.cache/
.eslintcache
.npm/
Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

+39
View File
@@ -0,0 +1,39 @@
# 프로젝트 개요
이 저장소는 **풀스택 모노레포 기본 템플릿**이다. 누구나 클론하여 바로 개발을 시작할 수 있도록 공통 규칙과 언어별 규칙을 정의한다.
| 디렉터리 | 스택 | 역할 | 개발 규칙 (스킬) |
| ----------- | -------------- | ------------------------ | ----------------------------------------- |
| `backend/` | NestJS (TS) | REST API 서버 | `/nestjs` → @.claude/skills/nestjs/SKILL.md |
| `frontend/` | Vue 3 (TS) | 웹 프론트엔드 | `/vue3` → @.claude/skills/vue3/SKILL.md |
| `flutter/` | Flutter (Dart) | 모바일 앱 | `/flutter` → @.claude/skills/flutter/SKILL.md |
| 루트 | Docker Compose | 로컬/운영 오케스트레이션 | `/docker` → @.claude/skills/docker/SKILL.md |
세 클라이언트는 동일한 **표준 응답 포맷**과 **에러 코드 맵**을 공유한다 (아래 공통 규칙 참조).
> 규칙은 `.claude/` 아래로 모듈화되어 있다.
> - `.claude/rules/` — 세 플랫폼 공통 규칙 (코딩·응답 포맷·에러 코드·커밋·보안·환경변수)
> - `.claude/skills/` — 도메인별 개발 규칙 (스킬). 슬래시 명령(`/nestjs` 등) 또는 해당 디렉터리 작업 시 자동 적용
> - `.claude/settings.json` — Claude Code 권한·환경 설정
---
## 공통 규칙 (모든 디렉터리 적용)
@.claude/rules/coding-common.md
@.claude/rules/response-format.md
@.claude/rules/error-codes.md
@.claude/rules/git-convention.md
@.claude/rules/security.md
@.claude/rules/env-structure.md
@.claude/rules/testing.md
@.claude/rules/checklist.md
---
## 도메인별 개발 규칙 (스킬)
- 백엔드(NestJS): @.claude/skills/nestjs/SKILL.md
- 프론트엔드(Vue 3): @.claude/skills/vue3/SKILL.md
- 모바일(Flutter): @.claude/skills/flutter/SKILL.md
- 실행/배포(Docker): @.claude/skills/docker/SKILL.md
+107
View File
@@ -0,0 +1,107 @@
# 풀스택 모노레포 기본 템플릿
NestJS(백엔드) · Vue 3(웹) · Flutter(모바일)를 하나의 저장소에서 운영하는 풀스택 템플릿입니다.
세 클라이언트는 **동일한 표준 응답 포맷**과 **공통 에러 코드 맵**을 공유하며, Docker Compose로 한 번에 구동됩니다.
## 구성
| 디렉터리 | 스택 | 역할 | 포트(기본) |
| ----------- | -------------- | ------------------------ | ---------- |
| `backend/` | NestJS + TypeORM | REST API 서버 | 3000 |
| `frontend/` | Vue 3 + Vite | 웹 프론트엔드 | 5173 |
| `flutter/` | Flutter + Riverpod | 모바일 앱 | - |
| `db` | PostgreSQL | 데이터베이스 | 5432 |
| `redis` | Redis | 캐시 / 세션 | 6379 |
## 사전 요구사항
- Docker / Docker Compose — backend·frontend·db·redis 구동
- Flutter SDK `^3.11` — 모바일 앱 실행용 (compose 대상 아님)
## 빠른 시작 (Docker)
```bash
# 1) 환경변수 파일 생성 (예제 복사)
# 실제 설정값은 루트 .env.development 한 곳에서 관리한다 (SSOT).
# backend/frontend 는 docker-compose 가 값을 주입하므로 예제만 복사하면 된다.
cp .env.development.example .env.development # ← 여기에만 값 입력
cp backend/.env.development.example backend/.env.development # (존재 필요: JWT 등 고유 시크릿)
# frontend 는 compose 가 빌드 시 값을 주입하므로 자체 env 복사 불필요
# 2) 개발 환경 기동 (코드만 수정 시)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d
# 3) 의존성 변경 시에만 --build
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d --build
```
기동 후:
- 웹: <http://localhost:5173>
- API: <http://localhost:3000/api>
- Swagger 문서: <http://localhost:3000/api-docs>
전체 실행/배포 명령어는 [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md) 참조.
## 모바일 앱 실행 (Flutter)
Flutter 는 Docker Compose 대상이 아니므로 로컬에서 직접 실행합니다.
```bash
cd flutter && cp .env.example .env && flutter pub get && flutter run
```
> `flutter/.env` 의 `API_URL` 을 백엔드 주소로 설정합니다. (Android 에뮬레이터는 `http://10.0.2.2:3000/api`)
## 환경변수
`.env*` 는 커밋하지 않습니다. 각 디렉터리의 `*.example` 파일을 복사해 사용하세요.
**루트 `.env.{env}` 가 단일 진실 공급원(SSOT)** 입니다. backend·frontend 는 docker-compose 가
루트 값을 주입하므로 자체 env 에 값을 중복 작성하지 않습니다. (자세한 위임 구조는 [`CLAUDE.md`](./CLAUDE.md) 의 "환경변수 구조" 참조)
| 파일 | 용도 | 값 작성 위치 |
| ----------------------------- | ---------------------------------------- | --------------------- |
| `.env.{env}.example` | 루트 — 모든 설정의 SSOT (compose 주입) | ✅ 여기에 작성 |
| `backend/.env.{env}.example` | 백엔드 — 고유 시크릿(JWT 등)만 | 최소 (존재 필수) |
| `frontend/.env.{env}.example` | 프론트 — 빌드 시 루트 `BACKEND_API_URL` 주입 | 작성 불필요 |
| `flutter/.env.example` | 모바일 — `API_URL` (compose 비대상) | ✅ 독립 관리 |
`{env}` = `development` | `production`
## 표준 응답 포맷
```jsonc
// 성공
{ "success": true, "data": { /* ... */ } }
// 실패
{ "success": false, "error": { "code": "VAL_001", "message": "..." } }
```
에러 코드 맵과 코딩 규칙 전문은 [`CLAUDE.md`](./CLAUDE.md) 및 `.claude/rules/`·`.claude/skills/` 의 규칙 문서를 참고하세요.
## 개발 규칙
| 영역 | 문서 |
| --------- | ------------------------------------------------- |
| 공통/보안 | [`CLAUDE.md`](./CLAUDE.md) · [`.claude/rules/`](./.claude/rules/) |
| NestJS | [`.claude/skills/nestjs/SKILL.md`](./.claude/skills/nestjs/SKILL.md) |
| Vue 3 | [`.claude/skills/vue3/SKILL.md`](./.claude/skills/vue3/SKILL.md) |
| Flutter | [`.claude/skills/flutter/SKILL.md`](./.claude/skills/flutter/SKILL.md) |
| Docker | [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md) |
## 디렉터리 구조
```
.
├── backend/ # NestJS API (Controller / Service / DTO / 공통 필터·인터셉터)
├── frontend/ # Vue 3 (pages / components / composables / stores / router)
├── flutter/ # Flutter (core / features / widgets)
├── docker-compose.yml # 베이스 정의
├── docker-compose.dev.yml # 개발 오버라이드 (핫리로드, 포트 노출)
├── CLAUDE.md # 프로젝트 개요 + 공통 규칙 @import
└── .claude/
├── settings.json # Claude Code 권한·환경 설정
├── rules/ # 공통 규칙 (코딩·응답 포맷·에러 코드·커밋·보안·환경변수)
└── skills/ # 도메인별 개발 규칙 (nestjs·vue3·flutter·docker)
```
View File
View File
+58
View File
@@ -0,0 +1,58 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.env.development
.env.production
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+4
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
+33
View File
@@ -0,0 +1,33 @@
FROM node:20-alpine AS builder
WORKDIR /app
# 호스트의 npm 11 이 작성한 package-lock.json 형식과 호환되도록 npm 을 11 로 업그레이드
RUN npm install -g npm@11
# 재현 가능한 빌드를 위해 npm ci 사용 (lockfile 기반)
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
# 운영 stage 도 동일 npm 버전 사용
RUN npm install -g npm@11
# 운영용 의존성만 설치
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
# 비-root 실행 (보안 규칙)
USER node
CMD ["node", "dist/main"]
+98
View File
@@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
+35
View File
@@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"builder": "swc",
"typeCheck": true
}
}
+13651
View File
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
{
"name": "backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.1.1",
"@nestjs/swagger": "^11.2.6",
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.1",
"cache-manager": "^6.4.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.4",
"express-rate-limit": "^8.3.2",
"helmet": "^8.1.0",
"nest-winston": "^1.10.0",
"pg": "^8.20.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.28",
"winston": "^3.17.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.8.1",
"@swc/core": "^1.15.24",
"chokidar": "^5.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
+16
View File
@@ -0,0 +1,16 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { AppService } from './app.service';
// 루트 컨트롤러 — 헬스체크/디버그 용 단순 엔드포인트
@ApiTags('App')
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello')
@ApiOperation({ summary: '동작 확인용 Hello World' })
getHello(): string {
return this.appService.getHello();
}
}
+72
View File
@@ -0,0 +1,72 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { CacheModule } from '@nestjs/cache-manager';
import { APP_GUARD } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HealthModule } from './modules/health/health.module';
import { UserModule } from './modules/user/user.module';
@Module({
imports: [
ScheduleModule.forRoot(),
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
}),
// 전역 캐시 매니저 — Redis 도입 시 store 옵션을 redisStore 로 변경
CacheModule.register({
isGlobal: true,
ttl: 60_000, // 60초 기본 TTL
}),
// 전역 Rate Limiter — main.ts 의 express-rate-limit 와 별개로 라우트 단위 제어 가능
ThrottlerModule.forRoot([
{
ttl: 60_000,
limit: 100,
},
]),
TypeOrmModule.forRootAsync({
useFactory: () => {
const isProduction = process.env.NODE_ENV === 'production';
return {
type: 'postgres',
host: process.env.DB_HOST || 'db',
port: parseInt(process.env.DB_PORT || '5432', 10),
username:
process.env.DB_USER || process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database:
process.env.DB_NAME || process.env.DB_DATABASE || 'project_db',
autoLoadEntities: true,
// 운영 환경에서는 절대 synchronize 사용 금지 (스키마 자동 변경 → 데이터 손실 위험)
synchronize: !isProduction,
migrationsRun: isProduction,
logging: !isProduction,
extra: {
max: 100,
connectionTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
},
entities: [__dirname + '/**/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/*{.ts,.js}'],
};
},
}),
HealthModule,
UserModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
+8
View File
@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
@@ -0,0 +1,75 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Response } from 'express';
// 전역 Exception Filter — CLAUDE.md 에러 코드 맵에 따라 표준 응답으로 변환
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
// 비교 대상인 HttpStatus enum 과 동일 타입으로 선언 (no-unsafe-enum-comparison 대응)
let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
let code = 'SYS_001';
let message =
'일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (status === HttpStatus.UNAUTHORIZED) {
code = 'AUTH_001';
message = '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.';
} else if (status === HttpStatus.FORBIDDEN) {
code = 'AUTH_003';
message = '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.';
} else if (status === HttpStatus.NOT_FOUND) {
code = 'RES_001';
message = '요청하신 정보나 페이지를 찾을 수 없습니다.';
} else if (
status === HttpStatus.BAD_REQUEST ||
status === HttpStatus.UNPROCESSABLE_ENTITY
) {
code = 'VAL_001';
message =
'입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)';
} else {
code = 'BIZ_001';
message = '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.';
// 비즈니스 예외에서는 사용자에게 보일 메시지를 재정의 가능
if (
typeof exceptionResponse === 'object' &&
exceptionResponse !== null &&
'message' in exceptionResponse
) {
const responseMessage = exceptionResponse.message;
if (typeof responseMessage === 'string') {
message = responseMessage;
}
}
}
} else {
// 정의되지 않은 예외 → 운영팀이 추적 가능하도록 로그 + SYS_001 응답 유지
this.logger.error('Unhandled Exception', exception as Error);
}
response.status(status).json({
success: false,
error: {
code,
message,
},
});
}
}
@@ -0,0 +1,33 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface StandardResponse<T> {
success: boolean;
// 핸들러가 undefined/null 을 반환하면 null 로 정규화되므로 nullable
data: T | null;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<
T,
StandardResponse<T>
> {
intercept(
_context: ExecutionContext,
next: CallHandler,
): Observable<StandardResponse<T>> {
// 이미 Http 응답객체인 경우 등 예외처리가 필요할 수 있으나 기본적으로 data 래핑
return (next.handle() as Observable<T>).pipe(
map((data) => ({
success: true,
data: data ?? null,
})),
);
}
}
+111
View File
@@ -0,0 +1,111 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger, RequestMethod, ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { WinstonModule } from 'nest-winston';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import type {
Express,
Request as ExpressRequest,
Response as ExpressResponse,
} from 'express';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
import { winstonConfig } from './shared/logger/winston.config';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger(winstonConfig),
});
// 리버스 프록시(Nginx, Docker 배포 환경 등) 뒤에서 올바른 클라이언트 IP 식별 지원을 위해 설정합니다.
const expressApp = app.getHttpAdapter().getInstance() as Express;
expressApp.set('trust proxy', 1);
// 글로벌 prefix — 모든 라우트가 /api/* 로 정규화됨 (Flutter, Web 양쪽 baseURL과 일치)
app.setGlobalPrefix('api', {
// 헬스체크 / 루트 상태 엔드포인트는 prefix 미적용
exclude: [{ path: '/', method: RequestMethod.GET }],
});
// 기본 주소('/') 접속 시 상태 확인용 텍스트 응답 추가
expressApp.get('/', (_req: ExpressRequest, res: ExpressResponse) => {
res.send('Backend API Server is running 정상 구동 중입니다.');
});
// 1. 보안 헤더 (Helmet) - XSS 방지 및 기본 웹 보안
app.use(helmet());
// 2. 화이트리스트 기반 CORS 정책
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
const corsOriginEnv = process.env.CORS_ORIGIN;
app.enableCors({
origin: (
origin: string | undefined,
callback: (err: Error | null, allow?: boolean) => void,
) => {
// 서버간의 통신(origin이 undefined)이거나 명시된 리스트일 때만 허용
const whitelist = [frontendUrl, 'http://localhost:3000'];
if (corsOriginEnv) whitelist.push(corsOriginEnv);
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
// 클라이언트에는 403 금지 에러로 리턴됨
callback(new Error('CORS 정책에 의해 차단된 도메인입니다.'));
}
},
credentials: true, // 쿠키 교환 허용
});
// 3. 글로벌 Rate Limiting 적용 (DDoS, 브루트포스 예방)
app.use(
rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 150, // 15분 IP당 최대 150개 요청
message: {
success: false,
error: {
code: 'SYS_001',
message:
'일시적인 시스템 오류가 발생했습니다. (요청 한도 초과) 잠시 후 다시 시도해 주세요.', // 글로벌 통합 에러 포맷
},
},
}),
);
// 4. 전역 DTO 유효성 파이프 (강력한 검증 단계)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // DTO에 불필요한 값이 오면 삭제
forbidNonWhitelisted: true, // DTO에 없는 필드가 주입되면 400 에러 발생
transform: true, // 네트워크 데이터를 DTO나 기본 타입으로 자동 변환
}),
);
// 5. 프론트엔드 연동을 위한 규격화된 에러 & 성공 페이로드 인터셉터 적용
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());
// 6. Swagger API 문서 설정
const config = new DocumentBuilder()
.setTitle('Backend API Documentation')
.setDescription('The API documentation conforming to the global guidelines')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
const port = Number(process.env.PORT ?? 3000);
await app.listen(port, '0.0.0.0');
Logger.log(`🚀 Backend running on http://localhost:${port}/api`, 'Bootstrap');
Logger.log(
`📘 Swagger docs at http://localhost:${port}/api-docs`,
'Bootstrap',
);
}
void bootstrap();
@@ -0,0 +1,24 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import {
HealthCheck,
HealthCheckService,
TypeOrmHealthIndicator,
} from '@nestjs/terminus';
// 헬스체크 엔드포인트 — Docker healthcheck, 모니터링 시스템에서 사용
@ApiTags('Health')
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
) {}
@Get()
@HealthCheck()
@ApiOperation({ summary: '애플리케이션 상태 확인' })
check() {
return this.health.check([() => this.db.pingCheck('database')]);
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';
@Module({
imports: [TerminusModule],
controllers: [HealthController],
})
export class HealthModule {}
@@ -0,0 +1,20 @@
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
// 사용자 생성 DTO 샘플 — 모든 검증 메시지는 한국어, Swagger 메타 필수
export class CreateUserDto {
@ApiProperty({ description: '사용자 이메일', example: 'user@example.com' })
@IsEmail({}, { message: '올바른 이메일 형식을 입력해 주세요.' })
@IsNotEmpty({ message: '이메일은 필수 입력 항목입니다.' })
email!: string;
@ApiProperty({ description: '비밀번호 (최소 8자)', example: 'password123' })
@IsString()
@MinLength(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' })
password!: string;
@ApiProperty({ description: '사용자 이름', example: '홍길동' })
@IsString()
@IsNotEmpty({ message: '이름은 필수 입력 항목입니다.' })
name!: string;
}
@@ -0,0 +1,40 @@
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Post,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
// 사용자 도메인 컨트롤러 샘플 — 라우팅과 입출력만 담당, 비즈니스 로직은 모두 service 위임
@ApiTags('User')
@ApiBearerAuth()
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
@ApiOperation({ summary: '사용자 단건 조회' })
@ApiResponse({ status: 200, description: '사용자 조회 성공' })
@ApiResponse({ status: 404, description: '사용자를 찾을 수 없음 (RES_001)' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
@Post()
@ApiOperation({ summary: '사용자 생성' })
@ApiResponse({ status: 201, description: '사용자 생성 성공' })
@ApiResponse({ status: 400, description: '입력값 검증 실패 (VAL_001)' })
create(@Body() dto: CreateUserDto) {
return this.userService.create(dto);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
+28
View File
@@ -0,0 +1,28 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
// 비즈니스 로직 계층 샘플 — 실제 구현 시 Repository 주입하여 데이터 접근
@Injectable()
export class UserService {
// 임시 인메모리 저장소 — Repository 도입 시 교체
private readonly users = new Map<
number,
{ id: number; email: string; name: string }
>();
findOne(id: number) {
const user = this.users.get(id);
if (!user) {
// 404 → 전역 필터에서 RES_001 로 변환됨
throw new NotFoundException();
}
return user;
}
create(dto: CreateUserDto) {
const id = this.users.size + 1;
const user = { id, email: dto.email, name: dto.name };
this.users.set(id, user);
return user;
}
}
@@ -0,0 +1,20 @@
import { utilities as nestWinstonUtilities } from 'nest-winston';
import * as winston from 'winston';
// 표준 로거 설정 — console.log 대체용
// 운영 환경에서는 파일 로테이션 또는 외부 로그 수집기(예: Loki, ELK) 추가 권장
export const winstonConfig: winston.LoggerOptions = {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
nestWinstonUtilities.format.nestLike('App', {
colors: process.env.NODE_ENV !== 'production',
prettyPrint: true,
}),
),
}),
],
};
+13
View File
@@ -0,0 +1,13 @@
// 공통 포맷터 — 부수효과 없는 순수 함수만 작성
/**
* Date 또는 ISO 문자열을 'YYYY-MM-DD' 형식으로 변환
*/
export function formatDate(input: Date | string): string {
const date = typeof input === 'string' ? new Date(input) : input;
if (Number.isNaN(date.getTime())) return '';
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
}
+25
View File
@@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
+9
View File
@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
+33
View File
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"watchOptions": {
"watchFile": "priorityPollingInterval",
"watchDirectory": "dynamicPriorityPolling",
"fallbackPolling": "dynamicPriority"
}
}
+53
View File
@@ -0,0 +1,53 @@
services:
frontend:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (수동 up 때만 실행)
restart: "no"
build:
target: build-stage
command: npm run dev -- --host 0.0.0.0 --port 80
ports:
- "${FRONTEND_PORT}:80"
environment:
- CHOKIDAR_USEPOLLING=true
- WATCHPACK_POLLING=true
volumes:
- ./${FRONTEND_DIR}:/app
- /app/node_modules
# dev에서는 리버스 프록시 미사용 — base의 networks 목록을 덮어씀
networks: !override
- app-network
backend:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (수동 up 때만 실행)
restart: "no"
build:
target: builder
command: npm run start:dev
ports:
- "${BACKEND_PORT}:${BACKEND_PORT}"
- "${WEBSOCKET_PORT}:${WEBSOCKET_PORT}"
environment:
- CHOKIDAR_USEPOLLING=true
- WATCHPACK_POLLING=true
volumes:
- ./${BACKEND_DIR}:/app
- /app/node_modules
# dev에서는 리버스 프록시 미사용 — base의 networks 목록을 덮어씀
networks: !override
- app-network
db:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (base 의 always 를 덮어씀)
restart: "no"
ports:
- "${DB_PORT}:5432"
redis:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (base 의 always 를 덮어씀)
restart: "no"
ports:
- "${REDIS_PORT}:6379"
# base의 proxy 외부 네트워크 의존을 dev에서 제거
networks:
proxy: !reset null
+145
View File
@@ -0,0 +1,145 @@
services:
# ----------------------------------------
# 1. Frontend (Nginx 정적 호스팅)
# ----------------------------------------
frontend:
container_name: ${PROJECT_NAME}-frontend
build:
context: ./${FRONTEND_DIR}
dockerfile: Dockerfile
args:
- BUILD_MODE=${BUILD_MODE}
- VITE_API_BASE_URL=${BACKEND_API_URL}
expose:
- "80"
environment:
- NODE_ENV=${APP_ENV}
restart: unless-stopped
networks:
- app-network
- proxy
depends_on:
- backend
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:80/ || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 10s
deploy:
resources:
limits:
cpus: '0.5'
memory: 512m
# ----------------------------------------
# 2. Backend (HTTP API & WebSocket 통합)
# ----------------------------------------
backend:
container_name: ${PROJECT_NAME}-backend
build:
context: ./${BACKEND_DIR}
dockerfile: Dockerfile
expose:
- "${BACKEND_PORT}"
- "${WEBSOCKET_PORT}"
env_file:
- ./${BACKEND_DIR}/.env.${APP_ENV}
environment:
- NODE_ENV=${APP_ENV}
- PORT=${BACKEND_PORT}
- WEBSOCKET_PORT=${WEBSOCKET_PORT}
- CORS_ORIGIN=${CORS_ORIGIN}
- DB_HOST=db
- DB_PORT=5432
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=${DB_NAME}
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
restart: unless-stopped
networks:
- app-network
- proxy
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:${BACKEND_PORT}/ || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
cpus: '1.0'
memory: 1g
# ----------------------------------------
# 3. Database (PostgreSQL) — 내부 네트워크 전용
# ----------------------------------------
db:
container_name: ${PROJECT_NAME}-db
image: ${DB_IMAGE}
expose:
- "5432"
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- ${DB_DATA_DIR}:/var/lib/postgresql/data
restart: always
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
deploy:
resources:
limits:
cpus: '0.5'
memory: 512m
labels:
- "com.centurylinklabs.watchtower.enable=true"
# ----------------------------------------
# 4. Redis — 내부 네트워크 전용
# ----------------------------------------
redis:
container_name: ${PROJECT_NAME}-redis
image: ${REDIS_IMAGE}
command: redis-server --requirepass ${REDIS_PASSWORD}
expose:
- "6379"
volumes:
- ${REDIS_DATA_DIR}:/data
restart: always
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep PONG"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
deploy:
resources:
limits:
cpus: '0.25'
memory: 256m
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
app-network:
driver: bridge
proxy:
external: true
+12
View File
@@ -0,0 +1,12 @@
# ==========================================
# Flutter 환경변수 (flutter_dotenv) — pubspec assets 에 .env 등록됨
# 사용법: cp .env.example .env
#
# ⚠️ 모바일 앱은 docker-compose 대상이 아니므로 루트 .env 의 위임을 받지 않는다.
# 아래 값은 항상 이 파일에서 직접 관리한다.
# 빌드 타임 분리가 필요하면 --dart-define 또는 .env.{flavor} 도입 고려.
# ==========================================
# 백엔드 API 베이스 URL (백엔드 글로벌 prefix /api 포함)
# 안드로이드 에뮬레이터에서 호스트 접근 시: http://10.0.2.2:3000/api
API_URL=http://localhost:3000/api
+54
View File
@@ -0,0 +1,54 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.env.development
.env.production
+45
View File
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: android
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: ios
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: linux
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: macos
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: web
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: windows
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+17
View File
@@ -0,0 +1,17 @@
# tma_app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.tma_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.tma_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="tma_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.tma_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1,620 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.tmaApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show More