git: 원격 저장소 연결 및 초기 커밋
Made-with: Cursor
This commit is contained in:
15
.cursor/rules/01-base.mdc
Normal file
15
.cursor/rules/01-base.mdc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
- 모든 응답과 문서는 한국어로 작성한다.
|
||||||
|
- 답변은 짧고 명확하게 작성한다.
|
||||||
|
- 이모지 사용을 금지한다.
|
||||||
|
- 추측성 답변을 하지 않는다.
|
||||||
|
- 불확실한 내용은 반드시 명시한다.
|
||||||
|
|
||||||
|
- 기존 코드 스타일을 우선 따른다.
|
||||||
|
- 불필요한 리팩토링을 금지한다.
|
||||||
|
- 요청되지 않은 구조 변경을 금지한다.
|
||||||
|
|
||||||
|
- 파일 생성은 최소화한다.
|
||||||
|
- 기존 코드에서 해결 가능한지 먼저 검토한다.
|
||||||
14
.cursor/rules/02-workflow.mdc
Normal file
14
.cursor/rules/02-workflow.mdc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
모든 작업은 반드시 아래 순서를 따른다:
|
||||||
|
|
||||||
|
1. docs 문서를 먼저 확인한다.
|
||||||
|
2. 관련 코드 파일을 확인한다.
|
||||||
|
3. 기존 구현 방식과 패턴을 파악한다.
|
||||||
|
4. 최소 범위로 수정한다.
|
||||||
|
5. 검증을 수행한다.
|
||||||
|
6. 문서를 즉시 갱신한다.
|
||||||
|
7. 누락 여부를 확인한다.
|
||||||
|
|
||||||
|
이 순서를 생략하거나 변경하지 않는다.
|
||||||
15
.cursor/rules/03-docs.mdc
Normal file
15
.cursor/rules/03-docs.mdc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
작업 후 반드시 문서를 갱신한다.
|
||||||
|
|
||||||
|
- update.md: 작업 내용을 항목형으로 기록 (~추가., ~수정.)
|
||||||
|
- todo.md: 현재 유효한 작업만 유지
|
||||||
|
- spec.md: API / 데이터 구조 변경 시 갱신
|
||||||
|
- history.md: 구조 변경 + 이유 기록
|
||||||
|
- map.md: 파일-기능 매핑 유지
|
||||||
|
- deploy.md: 실행 및 배포 방법 최신화
|
||||||
|
|
||||||
|
문서와 코드가 다르면:
|
||||||
|
1. 코드를 기준으로 판단
|
||||||
|
2. 문서를 수정한다
|
||||||
11
.cursor/rules/04-project.mdc
Normal file
11
.cursor/rules/04-project.mdc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
- 이 프로젝트는 Vite + Vue 3(SPA) 기반이다.
|
||||||
|
- 기존 구조, 네이밍, 패턴을 반드시 먼저 파악한다.
|
||||||
|
- 기존 composables, utils, components를 우선 재사용한다.
|
||||||
|
- 상태 관리는 기존 방식을 따른다.
|
||||||
|
- 동일 데이터를 중복 관리하지 않는다.
|
||||||
|
|
||||||
|
- Tailwind CSS를 사용한다.
|
||||||
|
- 전역 폰트는 Pretendard를 유지한다.
|
||||||
13
.cursor/rules/05-frontend.mdc
Normal file
13
.cursor/rules/05-frontend.mdc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
- API 호출은 기존 API 레이어를 사용한다.
|
||||||
|
- 정적 URL은 toApiUrl()로 처리한다.
|
||||||
|
|
||||||
|
- Composition API 패턴(ref, computed, onMounted)을 유지한다.
|
||||||
|
|
||||||
|
- UI는 "요약 + 필요 시 모달" 구조를 우선한다.
|
||||||
|
- 모달 활성화 시 body scroll lock을 적용한다.
|
||||||
|
|
||||||
|
- 리스트 변경 시 깜빡임 없이 즉시 반영한다.
|
||||||
|
- 애니메이션은 TransitionGroup을 사용한다.
|
||||||
11
.cursor/rules/06-backend.mdc
Normal file
11
.cursor/rules/06-backend.mdc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
- 입력 검증은 zod를 사용한다.
|
||||||
|
- 인증/권한은 PocketBase 클라이언트 래퍼 또는 라우터 가드 등으로 분리한다.
|
||||||
|
|
||||||
|
- 사용자 입력은 반드시 검증 후 저장한다.
|
||||||
|
- 환경 의존 값(URL 등)은 저장 전에 정규화한다.
|
||||||
|
|
||||||
|
- 업로드 파일명은 ASCII 안전 문자열을 사용한다.
|
||||||
|
- 관리자/사용자 업로드 경로를 분리한다.
|
||||||
12
.cursor/rules/07-style.mdc
Normal file
12
.cursor/rules/07-style.mdc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
- Tailwind CSS를 기본으로 사용한다.
|
||||||
|
- Tailwind만으로 구조를 구성하지 않는다.
|
||||||
|
- 주요 영역에는 고유 클래스명을 반드시 추가한다.
|
||||||
|
|
||||||
|
- 클래스명은 의미 기반으로 작성한다.
|
||||||
|
- 임시 이름 사용을 금지한다.
|
||||||
|
|
||||||
|
- JavaScript는 세미콜론 없이 작성한다.
|
||||||
|
- 주석은 필요한 경우에만 JSDoc 형식을 사용한다.
|
||||||
22
.cursor/rules/08-git.mdc
Normal file
22
.cursor/rules/08-git.mdc
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
- 커밋 메시지는 한국어로 작성한다.
|
||||||
|
- 형식: "영역: 변경 내용"
|
||||||
|
|
||||||
|
예:
|
||||||
|
docs: 규칙 정리
|
||||||
|
blog: 레이아웃 구현
|
||||||
|
|
||||||
|
- 작성자 정보:
|
||||||
|
name: zenn
|
||||||
|
email: zenn.message@gmail.com
|
||||||
|
|
||||||
|
- 작업 후 반드시:
|
||||||
|
git add -A
|
||||||
|
git commit
|
||||||
|
git push
|
||||||
|
|
||||||
|
- 버전은 v0.0.1 형식으로 증가시킨다.
|
||||||
|
|
||||||
|
- 푸시 전 민감 정보 포함 여부를 확인한다.
|
||||||
11
.cursor/rules/09-validation.mdc
Normal file
11
.cursor/rules/09-validation.mdc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
- 가능한 범위에서 기능 검증을 수행한다.
|
||||||
|
- 주요 사용자 흐름을 확인한다.
|
||||||
|
- null, undefined, 실패 응답을 점검한다.
|
||||||
|
|
||||||
|
- console.log는 디버깅용으로만 사용한다.
|
||||||
|
- 작업 완료 시 불필요한 로그는 제거한다.
|
||||||
|
|
||||||
|
- 검증을 수행하지 못한 경우 사유를 명시한다.
|
||||||
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
pb_data
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.md
|
||||||
|
.cursor
|
||||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# PocketBase 공개 URL (끝 슬래시 없이). Docker 배포 시 브라우저에서 접근 가능한 주소로 설정
|
||||||
|
VITE_POCKETBASE_URL=http://127.0.0.1:8090
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*.local
|
||||||
|
pb_data
|
||||||
|
*.log
|
||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG VITE_POCKETBASE_URL
|
||||||
|
ENV VITE_POCKETBASE_URL=${VITE_POCKETBASE_URL}
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
pocketbase:
|
||||||
|
image: ghcr.io/muchobien/pocketbase:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PB_HOST: 0.0.0.0
|
||||||
|
PB_PORT: 8090
|
||||||
|
ports:
|
||||||
|
- "8090:8090"
|
||||||
|
volumes:
|
||||||
|
- pocketbase_data:/pb_data
|
||||||
|
|
||||||
|
todo-web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
VITE_POCKETBASE_URL: ${VITE_POCKETBASE_URL:-http://127.0.0.1:8090}
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
depends_on:
|
||||||
|
- pocketbase
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pocketbase_data:
|
||||||
141
docs/convention.md
Normal file
141
docs/convention.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# 코딩 컨벤션
|
||||||
|
|
||||||
|
## 공통
|
||||||
|
- 시작 버전은 `v0.0.1`이다.
|
||||||
|
- 문서와 사용자 응답은 한국어로 작성한다.
|
||||||
|
- 작업 보고, 릴리스 메모, 주요 문서 업데이트에는 항상 버전명을 함께 표시한다.
|
||||||
|
- `docs/update.md`는 짧은 항목형으로 작성한다.
|
||||||
|
- `docs/update.md`는 서술형 문장보다 `~추가.`, `~수정.`, `~정리.`, `~진행.` 형식을 우선 사용한다.
|
||||||
|
- `docs/update.md`는 날짜가 아니라 버전 기준 섹션으로 구분한다.
|
||||||
|
- `docs/update.md`는 가장 최신 버전 섹션이 항상 맨 위에 오도록 유지한다.
|
||||||
|
- `docs/history.md`는 이유를 포함한 서술형 기록을 허용한다.
|
||||||
|
- `docs/history.md`는 가장 최신 항목이 항상 맨 위에 오도록 유지한다.
|
||||||
|
- `docs/history.md` 제목에는 날짜와 버전을 함께 적는다.
|
||||||
|
- JavaScript는 세미콜론 없이 현재 코드 스타일을 유지한다.
|
||||||
|
- 새 주석이 필요할 경우 JSDoc 형식을 사용한다.
|
||||||
|
- 경로, 주소, 운영 설정은 하드코딩보다 환경변수 기반 구성을 우선한다.
|
||||||
|
- 사이트 전역 기본 폰트는 항상 `Pretendard`를 우선 사용한다.
|
||||||
|
- Git 커밋 메시지는 한국어로 작성한다.
|
||||||
|
- 로컬 Git 작성자 정보는 항상 `zenn` / `zenn.message@gmail.com` 조합을 사용한다.
|
||||||
|
- 버전 릴리스가 포함된 작업은 `docs/update.md`의 버전 표기와 Git 태그를 함께 맞춘다.
|
||||||
|
- Git 푸시 전에는 민감 정보(실명, 개인 이메일, 비밀키, 로컬 절대 경로) 포함 여부를 다시 확인한다.
|
||||||
|
- 작업이 끝난 뒤에는 문서 업데이트까지 포함한 변경사항을 커밋하고 원격 저장소에 푸시한다.
|
||||||
|
|
||||||
|
### Git 업로드 양식
|
||||||
|
- 작성자 정보 확인: `git config user.name` / `git config user.email`
|
||||||
|
- 작성자 정보 고정: `git config user.name "zenn"` / `git config user.email "zenn.message@gmail.com"`
|
||||||
|
- 작업 확인: `git status`
|
||||||
|
- 변경 스테이징: `git add -A`
|
||||||
|
- 커밋: `git commit -m "작업 목적을 한 줄로 설명하는 한국어 메시지"`
|
||||||
|
- 원격 푸시: `git push origin main`
|
||||||
|
- 푸시 후 확인: `git status`, `git log -1 --oneline`
|
||||||
|
|
||||||
|
### Git 커밋 메시지 규칙
|
||||||
|
- 한국어 한 줄 요약으로 작성한다.
|
||||||
|
- 가능하면 `영역: 변경 내용` 형식을 사용한다.
|
||||||
|
- 예시: `docs: Git 업로드 규칙 정리`
|
||||||
|
- 예시: `app: 기본 Vite 레이아웃 초기 구현`
|
||||||
|
|
||||||
|
### 버전 표기 규칙
|
||||||
|
- 작업이 끝나 커밋할 때마다 버전을 증가시킨다.
|
||||||
|
- 새 기능 시작, 구조 변경, 배포 준비, 작업 결과 보고 시 현재 버전을 함께 적는다.
|
||||||
|
- 형식은 항상 `v0.0.1`처럼 `v` 접두사를 포함한다.
|
||||||
|
- 버전이 변경되면 `docs/update.md`, `docs/spec.md`, `docs/history.md`, `docs/deploy.md`에 함께 반영한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 수정 범위 규칙
|
||||||
|
- 요청된 기능과 직접 관련 없는 코드는 수정하지 않는다.
|
||||||
|
- 기존에 정상 동작하는 로직은 리팩토링하지 않는다.
|
||||||
|
- 스타일 변경만으로 해결 가능한 경우 로직 수정 금지.
|
||||||
|
- 기존 API 구조를 임의로 변경하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 환경 변수 및 설정 규칙
|
||||||
|
- API 주소, 파일 경로, 외부 서비스 URL은 반드시 환경변수로 관리한다.
|
||||||
|
- 코드 내에 직접 문자열로 작성하지 않는다.
|
||||||
|
- 예외적으로 테스트용 임시 값은 주석으로 명시한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 로그 및 디버깅 규칙
|
||||||
|
- console.log는 디버깅 용도로만 사용한다.
|
||||||
|
- 작업 완료 시 불필요한 로그는 반드시 제거한다.
|
||||||
|
- 운영 환경에 영향을 줄 수 있는 로그는 추가하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 프런트엔드
|
||||||
|
|
||||||
|
### 기본 구조
|
||||||
|
- API 호출은 `src/lib/pocketBase.js` 및 관련 composable을 통해 통합한다.
|
||||||
|
- 정적 파일 URL 조합은 `toApiUrl()`로 처리한다.
|
||||||
|
- 화면 상태는 `ref`, `computed`, `onMounted` 중심의 단순한 Composition API 패턴을 유지한다.
|
||||||
|
|
||||||
|
### UI/UX 패턴
|
||||||
|
- 설정/계정과 같이 자주 변경되지 않는 정보는 `현재 상태 요약 + 필요 시 모달 편집` 구조를 우선한다.
|
||||||
|
- 모달이 활성화된 동안에는 `body` 스크롤 잠금을 적용한다.
|
||||||
|
- 배경 화면이 움직이지 않도록 한다.
|
||||||
|
- 정렬 가능한 리스트는 즉시 재렌더링으로 깜빡이지 않도록 한다.
|
||||||
|
- `TransitionGroup` 등을 사용해 이동 애니메이션을 적용한다.
|
||||||
|
|
||||||
|
### 스타일 (Tailwind + 구조 클래스)
|
||||||
|
- 기본 스타일링은 Tailwind CSS를 사용한다.
|
||||||
|
- Tailwind 유틸리티 클래스만으로 구조를 구성하지 않는다.
|
||||||
|
- 주요 영역에는 반드시 의미 있는 고유 클래스명을 함께 작성한다.
|
||||||
|
- 고유 클래스명은 개발자 도구에서 구조를 빠르게 파악하기 위한 용도로 사용한다.
|
||||||
|
- 클래스명은 컴포넌트 또는 역할 기준으로 명확하게 작성한다.
|
||||||
|
- 임시 이름 사용을 금지한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 백엔드
|
||||||
|
|
||||||
|
### 기본 규칙
|
||||||
|
- 라우트 검증은 `zod`로 처리한다.
|
||||||
|
- 인증/권한 분기는 PocketBase 세션·가드(`requireAuth` 등)로 분리한다.
|
||||||
|
|
||||||
|
### 데이터 처리
|
||||||
|
- DB 저장 전 배포 환경에 종속되는 값(예: 로컬 절대 URL)을 제거하거나 정규화한다.
|
||||||
|
- 사용자 입력 데이터는 반드시 검증 후 저장한다.
|
||||||
|
|
||||||
|
### 파일 업로드
|
||||||
|
- 업로드 파일명은 ASCII 안전 문자열을 사용한다.
|
||||||
|
- 관리자 데이터와 사용자 데이터는 업로드 경로를 분리한다.
|
||||||
|
|
||||||
|
### 기능 흐름
|
||||||
|
- 사용자 프로필과 같이 “선택 후 저장” 흐름이 필요한 기능은
|
||||||
|
파일 선택과 실제 저장 요청을 분리한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 네이밍 규칙
|
||||||
|
- 변수명, 함수명, 클래스명은 의미가 명확하게 드러나야 한다.
|
||||||
|
- 축약어 남용을 금지한다.
|
||||||
|
- 임시 이름(`temp`, `data2`, `testFn`, `aaa`) 사용을 금지한다.
|
||||||
|
- 컴포넌트 이름은 역할 기반으로 작성한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 및 구조 규칙
|
||||||
|
- 파일 생성은 최소화한다.
|
||||||
|
- 기존 파일에서 해결 가능한지 먼저 검토한다.
|
||||||
|
- 사용하지 않는 코드(import, 함수, 스타일)는 반드시 제거한다.
|
||||||
|
- 파일은 하나의 책임을 가지도록 유지한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주석 규칙
|
||||||
|
- 주석은 필요한 경우에만 작성한다.
|
||||||
|
- 함수, 복잡한 로직에는 JSDoc 형식을 사용한다.
|
||||||
|
- 코드로 설명 가능한 내용은 주석으로 반복하지 않는다.
|
||||||
|
- 오래되면 잘못될 수 있는 주석은 작성하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 검증 규칙
|
||||||
|
- 가능한 범위에서 기능 검증을 수행한다.
|
||||||
|
- 주요 사용자 흐름을 확인한다.
|
||||||
|
- 에러 발생 가능성이 있는 조건(null, undefined, 실패 응답 등)을 점검한다.
|
||||||
|
- 검증을 수행하지 못한 경우 그 사유를 명확히 남긴다.
|
||||||
49
docs/deploy.md
Normal file
49
docs/deploy.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 배포 가이드
|
||||||
|
|
||||||
|
## 현재 버전
|
||||||
|
|
||||||
|
- `v0.0.1`
|
||||||
|
|
||||||
|
## 로컬 개발
|
||||||
|
|
||||||
|
1. `cp .env.example .env` 후 `VITE_POCKETBASE_URL`을 실제 PocketBase 주소로 맞춘다.
|
||||||
|
2. `npm install`
|
||||||
|
3. `npm run dev`
|
||||||
|
|
||||||
|
PocketBase는 Docker로 띄우려면 `docker compose up pocketbase`만 실행해도 된다(기본 `8090`).
|
||||||
|
|
||||||
|
## Docker Compose(웹 + PocketBase)
|
||||||
|
|
||||||
|
프로젝트 루트에서:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export VITE_POCKETBASE_URL="http://<이_기기_IP>:8090"
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
- 웹: `http://<이_기기_IP>:8080`
|
||||||
|
- PocketBase 관리자: `http://<이_기기_IP>:8090/_/`
|
||||||
|
|
||||||
|
**중요:** `VITE_POCKETBASE_URL`은 **컨테이너가 아니라 브라우저**에서 접근한다. 같은 PC에서만 쓸 때는 `http://127.0.0.1:8090`이 가능하지만, 휴대폰 등 다른 기기에서 접속할 때는 NAS/PC의 LAN IP 또는 공인 도메인을 사용해야 한다.
|
||||||
|
|
||||||
|
### PocketBase 초기 설정 요약
|
||||||
|
|
||||||
|
1. 관리자 UI에서 컬렉션 `todos` 생성(`title` text, `done` bool).
|
||||||
|
2. API 규칙을 배포 방식에 맞게 설정(인증 사용 시 로그인 사용자만 생성·수정 가능 등).
|
||||||
|
3. CORS 허용 출처에 웹 앱 출처(예: `http://<IP>:8080`)를 추가한다.
|
||||||
|
|
||||||
|
### 선택: 관리자 자동 생성
|
||||||
|
|
||||||
|
`docker-compose.yml`의 `pocketbase` 서비스에 `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` 환경 변수를 추가할 수 있다. 값은 저장소에 넣지 말고 NAS 비밀 관리 방식으로 주입한다.
|
||||||
|
|
||||||
|
## 단일 이미지(웹만)
|
||||||
|
|
||||||
|
`Dockerfile`은 정적 빌드 결과를 nginx로 제공한다. 빌드 시점에 `VITE_POCKETBASE_URL`이 번들에 포함된다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg VITE_POCKETBASE_URL="https://pb.example.com" -t todo-web .
|
||||||
|
```
|
||||||
|
|
||||||
|
## PWA·HTTPS
|
||||||
|
|
||||||
|
홈 화면 추가·서비스 워커 동작은 브라우저마다 다르며, **HTTPS**를 쓰는 것이 안정적이다. NAS 리버스 프록시에서 TLS를 종료하는 구성을 권장한다.
|
||||||
5
docs/history.md
Normal file
5
docs/history.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-13 · v0.0.1 — Vite SPA 스택 확정
|
||||||
|
|
||||||
|
SEO 요구가 없고 NAS·PWA·PocketBase 중심 배포가 목표이므로 Nuxt 대신 **Vite + Vue 3 SPA**로 시작한다. 빌드 산출물이 단순해지고 nginx 정적 호스팅과 Docker 구성이 단순해진다.
|
||||||
20
docs/map.md
Normal file
20
docs/map.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 파일-기능 매핑
|
||||||
|
|
||||||
|
## 현재 버전
|
||||||
|
|
||||||
|
- `v0.0.1`
|
||||||
|
|
||||||
|
| 경로 | 역할 |
|
||||||
|
| ------------------------- | ----------------------------------------- |
|
||||||
|
| `src/App.vue` | 미리 알림 스타일 UI, 목록·입력 |
|
||||||
|
| `src/main.js` | 앱 부트스트랩, PWA 서비스 워커 등록 |
|
||||||
|
| `src/style.css` | Tailwind 베이스, 모션 감소 대응 |
|
||||||
|
| `src/lib/apiUrl.js` | `toApiUrl()` URL 정규화 |
|
||||||
|
| `src/lib/pocketBase.js` | PocketBase 싱글톤 클라이언트 |
|
||||||
|
| `src/lib/todoSchema.js` | 할 일 제목 Zod 스키마 |
|
||||||
|
| `src/composables/useTodos.js` | 목록 로드·추가·완료 토글 |
|
||||||
|
| `vite.config.js` | Vue 플러그인, PWA 매니페스트 |
|
||||||
|
| `tailwind.config.js` | 테마 색·폰트 |
|
||||||
|
| `docker-compose.yml` | PocketBase + 정적 웹(nginx) 구성 |
|
||||||
|
| `Dockerfile` | Vite 빌드 후 nginx 이미지 |
|
||||||
|
| `nginx.conf` | SPA 폴백 라우팅 |
|
||||||
32
docs/spec.md
Normal file
32
docs/spec.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 기술 명세
|
||||||
|
|
||||||
|
## 현재 버전
|
||||||
|
|
||||||
|
- `v0.0.1`
|
||||||
|
|
||||||
|
## 스택
|
||||||
|
|
||||||
|
- Vue 3 + Vite(SPA)
|
||||||
|
- Tailwind CSS, Pretendard(변수 폰트, CDN)
|
||||||
|
- PWA: `vite-plugin-pwa`(자동 업데이트 등록)
|
||||||
|
- 데이터: PocketBase(공식 JS SDK)
|
||||||
|
- 입력 검증: Zod(`src/lib/todoSchema.js`)
|
||||||
|
|
||||||
|
## PocketBase 컬렉션: `todos`
|
||||||
|
|
||||||
|
| 필드 | 타입 | 설명 |
|
||||||
|
| ------ | ------- | ----------- |
|
||||||
|
| `title` | text | 할 일 제목 |
|
||||||
|
| `done` | bool | 완료 여부 |
|
||||||
|
|
||||||
|
규칙(API 규칙)은 운영 환경에 맞게 설정한다. 로컬 개발 시에는 본인 계정에 맞는 생성·수정 권한이 있어야 한다.
|
||||||
|
|
||||||
|
## 환경 변수
|
||||||
|
|
||||||
|
| 이름 | 설명 |
|
||||||
|
| ------------------------ | ------------------------------------------------------------ |
|
||||||
|
| `VITE_POCKETBASE_URL` | PocketBase 루트 URL(끝 슬래시 없음). **브라우저가 접근 가능한 주소**여야 한다. |
|
||||||
|
|
||||||
|
## 버전 정책
|
||||||
|
|
||||||
|
- 앱 버전은 `package.json`의 `version`과 문서의 `v0.0.1` 형식을 맞춘다.
|
||||||
5
docs/todo.md
Normal file
5
docs/todo.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 할 일 및 이슈
|
||||||
|
|
||||||
|
- PocketBase `todos` 컬렉션·API 규칙을 실제 배포 환경에 맞게 정리한다.
|
||||||
|
- 미리 알림 스타일 마이크로 인터랙션(체크·삭제·섹션)을 다듬는다.
|
||||||
|
- HTTPS 리버스 프록시 예시를 NAS 환경에 맞게 보강한다.
|
||||||
9
docs/update.md
Normal file
9
docs/update.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## v0.0.1
|
||||||
|
|
||||||
|
- ~추가. Vite + Vue 3 SPA, Tailwind, PWA, PocketBase 연동 골격.
|
||||||
|
- ~추가. Docker Compose(PocketBase + nginx 정적 웹).
|
||||||
|
- ~수정. 프로젝트 규칙을 Vite SPA 기준으로 정리.
|
||||||
|
- ~수정. 완료 표시는 SVG 아이콘으로 정리.
|
||||||
|
- ~추가. Git 저장소 초기화 및 원격 `origin`(git.sori.studio) 연결.
|
||||||
21
index.html
Normal file
21
index.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#f2f2f7" />
|
||||||
|
<meta name="description" content="NAS용 Todo" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
as="style"
|
||||||
|
crossorigin
|
||||||
|
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
|
||||||
|
/>
|
||||||
|
<title>Todo</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface-subtle text-ink antialiased">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
jsconfig.json
Normal file
11
jsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.js", "src/**/*.vue", "src/vite-env.d.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
10
nginx.conf
Normal file
10
nginx.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
6943
package-lock.json
generated
Normal file
6943
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "todo",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pocketbase": "^0.25.1",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"vite": "^5.4.14",
|
||||||
|
"vite-plugin-pwa": "^0.21.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect width="64" height="64" rx="14" fill="#007AFF"/>
|
||||||
|
<path stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M20 33l8 8 16-20"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 245 B |
135
src/App.vue
Normal file
135
src/App.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useTodos } from '@/composables/useTodos'
|
||||||
|
|
||||||
|
const { items, status, errorMessage, formError, newTitle, add, toggleDone } = useTodos()
|
||||||
|
|
||||||
|
const isLoading = computed(() => status.value === 'loading')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="todo-app-shell mx-auto flex min-h-full max-w-lg flex-col px-4 pb-10 pt-8 sm:px-6">
|
||||||
|
<header class="todo-app-header mb-8">
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wide text-ink-muted">내 목록</p>
|
||||||
|
<h1 class="mt-1 text-3xl font-semibold tracking-tight text-ink">할 일</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="todo-app-main flex flex-1 flex-col gap-6">
|
||||||
|
<section
|
||||||
|
class="todo-app-card rounded-3xl bg-surface p-4 shadow-card ring-1 ring-line/60 sm:p-5"
|
||||||
|
aria-label="새 할 일"
|
||||||
|
>
|
||||||
|
<form class="todo-app-form flex flex-col gap-3" @submit="add">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<input
|
||||||
|
v-model="newTitle"
|
||||||
|
class="todo-app-input min-h-11 flex-1 rounded-2xl border border-line bg-surface-subtle px-4 text-base text-ink outline-none ring-accent/30 transition focus:border-accent focus:ring-2"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="새로운 미리 알림"
|
||||||
|
maxlength="500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="todo-app-submit inline-flex min-h-11 flex-none items-center justify-center rounded-2xl bg-accent px-4 text-sm font-semibold text-white shadow-sm transition hover:brightness-105 active:scale-[0.98]"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="formError"
|
||||||
|
class="todo-app-form-error text-sm text-red-600"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
{{ formError }}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="todo-app-list-wrap" aria-live="polite">
|
||||||
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="todo-app-status rounded-2xl bg-surface/80 px-4 py-3 text-sm text-ink-muted ring-1 ring-line/50"
|
||||||
|
>
|
||||||
|
불러오는 중…
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="errorMessage"
|
||||||
|
class="todo-app-status rounded-2xl bg-surface px-4 py-3 text-sm text-ink ring-1 ring-line/60"
|
||||||
|
>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TransitionGroup
|
||||||
|
v-else
|
||||||
|
name="todo-list"
|
||||||
|
tag="ul"
|
||||||
|
class="todo-app-list divide-y divide-line/70 overflow-hidden rounded-3xl bg-surface shadow-card ring-1 ring-line/60"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
class="todo-app-row group flex items-center gap-3 px-4 py-3.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="todo-app-check flex h-6 w-6 flex-none items-center justify-center rounded-full border border-line bg-surface transition group-hover:border-accent/50"
|
||||||
|
:class="item.done ? 'border-accent bg-accent' : ''"
|
||||||
|
:aria-pressed="item.done"
|
||||||
|
:aria-label="item.done ? '완료 취소' : '완료로 표시'"
|
||||||
|
@click="toggleDone(item)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="item.done"
|
||||||
|
class="h-3.5 w-3.5 text-white"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.5 8.5 6.5 11.5 12.5 4.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
class="todo-app-title flex-1 text-base transition"
|
||||||
|
:class="item.done ? 'text-ink-muted line-through decoration-line' : 'text-ink'"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</TransitionGroup>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="!isLoading && !errorMessage && items.length === 0"
|
||||||
|
class="todo-app-empty mt-4 text-center text-sm text-ink-muted"
|
||||||
|
>
|
||||||
|
표시할 항목이 없습니다. PocketBase에 `todos` 컬렉션을 만든 뒤 다시 시도하세요.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.todo-list-move,
|
||||||
|
.todo-list-enter-active,
|
||||||
|
.todo-list-leave-active {
|
||||||
|
transition: opacity 0.22s ease, transform 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list-enter-from,
|
||||||
|
.todo-list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-app-list {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
src/composables/useTodos.js
Normal file
85
src/composables/useTodos.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { getPocketBase } from '@/lib/pocketBase'
|
||||||
|
import { parseTodoTitle } from '@/lib/todoSchema'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PocketBase `todos` 컬렉션과 연동한다. 스키마는 `docs/spec.md` 참고.
|
||||||
|
*/
|
||||||
|
export function useTodos() {
|
||||||
|
const items = ref([])
|
||||||
|
const status = ref('idle')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const formError = ref('')
|
||||||
|
const newTitle = ref('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
status.value = 'loading'
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
const pb = getPocketBase()
|
||||||
|
const list = await pb.collection('todos').getFullList({
|
||||||
|
sort: 'created'
|
||||||
|
})
|
||||||
|
items.value = list
|
||||||
|
status.value = 'idle'
|
||||||
|
} catch (err) {
|
||||||
|
status.value = 'error'
|
||||||
|
errorMessage.value = err?.message || '목록을 불러오지 못했습니다.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SubmitEvent} event
|
||||||
|
*/
|
||||||
|
async function add(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
formError.value = ''
|
||||||
|
try {
|
||||||
|
const { title } = parseTodoTitle({ title: newTitle.value })
|
||||||
|
const pb = getPocketBase()
|
||||||
|
const created = await pb.collection('todos').create({
|
||||||
|
title,
|
||||||
|
done: false
|
||||||
|
})
|
||||||
|
items.value = [...items.value, created]
|
||||||
|
newTitle.value = ''
|
||||||
|
} catch (err) {
|
||||||
|
const zodMessage = Array.isArray(err?.issues) ? err.issues[0]?.message : ''
|
||||||
|
if (zodMessage) {
|
||||||
|
formError.value = zodMessage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formError.value = err?.message || '추가하지 못했습니다.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('pocketbase').RecordModel} record
|
||||||
|
*/
|
||||||
|
async function toggleDone(record) {
|
||||||
|
try {
|
||||||
|
const pb = getPocketBase()
|
||||||
|
const updated = await pb.collection('todos').update(record.id, {
|
||||||
|
done: !record.done
|
||||||
|
})
|
||||||
|
items.value = items.value.map((row) => (row.id === updated.id ? updated : row))
|
||||||
|
} catch (err) {
|
||||||
|
formError.value = err?.message || '상태를 바꾸지 못했습니다.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
status,
|
||||||
|
errorMessage,
|
||||||
|
formError,
|
||||||
|
newTitle,
|
||||||
|
load,
|
||||||
|
add,
|
||||||
|
toggleDone
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/lib/apiUrl.js
Normal file
13
src/lib/apiUrl.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* API·정적 자원용 베이스 URL과 경로를 결합한다. 끝 슬래시는 정규화한다.
|
||||||
|
* @param {string | undefined} base
|
||||||
|
* @param {string} [path]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function toApiUrl(base, path = '') {
|
||||||
|
if (!base) return ''
|
||||||
|
const trimmed = base.replace(/\/+$/, '')
|
||||||
|
if (!path) return trimmed
|
||||||
|
const normalized = path.startsWith('/') ? path : `/${path}`
|
||||||
|
return `${trimmed}${normalized}`
|
||||||
|
}
|
||||||
19
src/lib/pocketBase.js
Normal file
19
src/lib/pocketBase.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import PocketBase from 'pocketbase'
|
||||||
|
import { toApiUrl } from '@/lib/apiUrl'
|
||||||
|
|
||||||
|
/** @type {PocketBase | null} */
|
||||||
|
let client = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {PocketBase}
|
||||||
|
*/
|
||||||
|
export function getPocketBase() {
|
||||||
|
const base = import.meta.env.VITE_POCKETBASE_URL
|
||||||
|
if (!base) {
|
||||||
|
throw new Error('VITE_POCKETBASE_URL이 설정되지 않았습니다.')
|
||||||
|
}
|
||||||
|
if (!client) {
|
||||||
|
client = new PocketBase(toApiUrl(base))
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
13
src/lib/todoSchema.js
Normal file
13
src/lib/todoSchema.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const todoTitleSchema = z.object({
|
||||||
|
title: z.string().trim().min(1, '제목을 입력하세요.').max(500, '제목이 너무 깁니다.')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {unknown} input
|
||||||
|
* @returns {{ title: string }}
|
||||||
|
*/
|
||||||
|
export function parseTodoTitle(input) {
|
||||||
|
return todoTitleSchema.parse(input)
|
||||||
|
}
|
||||||
8
src/main.js
Normal file
8
src/main.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
registerSW({ immediate: true })
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
||||||
22
src/style.css
Normal file
22
src/style.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/vite-env.d.ts
vendored
Normal file
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pwa/client" />
|
||||||
27
tailwind.config.js
Normal file
27
tailwind.config.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{vue,js}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Pretendard Variable', 'Pretendard', 'system-ui', 'sans-serif']
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
ink: {
|
||||||
|
DEFAULT: '#1c1c1e',
|
||||||
|
muted: '#636366'
|
||||||
|
},
|
||||||
|
surface: {
|
||||||
|
DEFAULT: '#ffffff',
|
||||||
|
subtle: '#f2f2f7'
|
||||||
|
},
|
||||||
|
line: '#d1d1d6',
|
||||||
|
accent: '#007aff'
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
card: '0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.04)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
}
|
||||||
40
vite.config.js
Normal file
40
vite.config.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
includeAssets: ['favicon.svg'],
|
||||||
|
manifest: {
|
||||||
|
name: 'Todo',
|
||||||
|
short_name: 'Todo',
|
||||||
|
description: 'NAS용 Todo 웹앱',
|
||||||
|
theme_color: '#f2f2f7',
|
||||||
|
background_color: '#f2f2f7',
|
||||||
|
display: 'standalone',
|
||||||
|
start_url: '/',
|
||||||
|
lang: 'ko',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/favicon.svg',
|
||||||
|
sizes: 'any',
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
purpose: 'any'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,svg,woff2}']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user