diff --git a/.github/ISSUE_TEMPLATE/general_issue.md b/.github/ISSUE_TEMPLATE/general_issue.md new file mode 100644 index 0000000..910fdca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general_issue.md @@ -0,0 +1,16 @@ +--- +name: General Issue +about: 기존 템플릿에 맞지 않는 새로운 유형의 이슈를 제보하거나 논의하고 싶을 때 사용해주세요. +title: '[유형] 제목입력' +labels: '' +assignees: '' +--- + +## 📝 이슈 설명 + + +## 👀 기대 동작 + + +## 🔍 참고 사항 (선택) + diff --git a/.github/workflows/deploy-extension.yml b/.github/workflows/deploy-extension.yml new file mode 100644 index 0000000..fe7ce3b --- /dev/null +++ b/.github/workflows/deploy-extension.yml @@ -0,0 +1,95 @@ +name: Deploy-Chrome-Extension + +on: + push: + branches: + - production + +jobs: + build: + name: Build Chrome Extension + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout Git repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + # manifest.json의 버전을 자동으로 업데이트 (build 번호만 증가) + - name: Auto bump manifest version + version_name + run: yarn extension-version + + # 빌드 실행 + - name: Build project + run: yarn build + + # 빌드된 dist + manifest.json 압축 + - name: Archive dist folder into zip + run: zip -r chrome-extension-${{ github.sha }}.zip dist + + # zip 파일을 GitHub Actions artifact로 업로드 (다음 job에서 사용) + - name: Upload zip as artifact + uses: actions/upload-artifact@v4 + with: + name: chrome-extension + path: chrome-extension-${{ github.sha }}.zip + + upload: + name: Upload to Chrome Web Store + runs-on: ubuntu-latest + needs: build + + strategy: + matrix: + node-version: [20.x] + + env: + EXTENSION_ID: ${{ secrets.EXTENSION_ID }} + + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + # 앞 단계에서 업로드한 artifact 다운로드 + - name: Download built artifact + uses: actions/download-artifact@v4 + with: + name: chrome-extension + + # Chrome Web Store CLI 설치 + - name: Install chrome-webstore-upload-cli + run: yarn global add chrome-webstore-upload-cli + + # 확장 프로그램 업로드 + - name: Upload extension zip + run: | + ZIP_FILE=$(ls *.zip) + chrome-webstore-upload upload \ + --source $ZIP_FILE \ + --extension-id ${{ secrets.EXTENSION_ID }} \ + --client-id ${{ secrets.CI_GOOGLE_CLIENT_ID }} \ + --client-secret ${{ secrets.CI_GOOGLE_CLIENT_SECRET }} \ + --refresh-token ${{ secrets.CI_GOOGLE_REFRESH_TOKEN }} + + # 업로드 후 Web Store에 게시(publish) + - name: Publish extension + run: | + chrome-webstore-upload publish \ + --extension-id ${{ secrets.EXTENSION_ID }} \ + --client-id ${{ secrets.CI_GOOGLE_CLIENT_ID }} \ + --client-secret ${{ secrets.CI_GOOGLE_CLIENT_SECRET }} \ + --refresh-token ${{ secrets.CI_GOOGLE_REFRESH_TOKEN }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2fec8b1..9c4cf2a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,24 +1,19 @@ -# 워크플로 이름: GitHub Actions 탭에서 보이는 이름 name: Deploy-Production -# 언제 실행할지 정의: main 브랜치에 push될 때 자동 실행됨 on: push: branches: - - main + - production jobs: build: - # 어떤 환경에서 실행할지 지정 (Ubuntu 최신 LTS) runs-on: ubuntu-latest - # 여러 버전의 Node.js로 실행 가능하도록 matrix 전략 사용 strategy: matrix: - node-version: [20.x] # 현재는 Node 20만 지정 + node-version: [20.x] steps: - # GitHub 레포지토리 코드 체크아웃 - name: Checkout Git repository uses: actions/checkout@v4 diff --git a/.husky/commit-msg b/.husky/commit-msg old mode 100644 new mode 100755 index 8e1f76a..92ab7e0 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,5 +1,8 @@ #!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" + +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" +nvm use >/dev/null npx --no-install commitlint --edit "$1" || { echo "\n❌ 커밋 메시지가 규칙을 따르지 않습니다! 🚨" diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 index b946a2c..33922f3 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,7 @@ #!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" -yarn lint-staged \ No newline at end of file +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" +nvm use >/dev/null + +yarn lint-staged diff --git a/.prettierignore b/.prettierignore index 4cbee9a..1583448 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ node_modules -package.json \ No newline at end of file +package.json +*.svg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdac969..833f910 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,10 @@ -# Delook 프로젝트 기여 가이드 +# Delook(디룩) 프로젝트 기여 가이드 -Delook 오픈소스 프로젝트에 관심을 가져주셔서 감사합니다. 이 문서는 Delook 프로젝트에 기여하는 방법과 절차를 안내하며, 원활한 협업을 위해 지켜야 할 규칙들을 정리했습니다. 기여 전에 아래 가이드라인을 읽고 따라 주시기 바랍니다. +Delook(디룩) 오픈소스 프로젝트에 관심을 가져주셔서 감사합니다. + +이 문서는 디룩에 기여하는 방법과 절차를 안내하며, 원활한 협업을 위해 지켜야 할 규칙들을 정리했습니다. + +기여 전에 아래 가이드라인을 읽고 따라 주시기 바랍니다. ## 목차 @@ -11,26 +15,35 @@ Delook 오픈소스 프로젝트에 관심을 가져주셔서 감사합니다. - [문서 작성 가이드라인](#문서-작성-가이드라인) - [문서 작성 스타일 가이드라인](#문서-작성-스타일-가이드) - [템플릿 사용 안내](#템플릿-사용-안내) +- [이미지 삽입 가이드](#이미지-삽입-가이드) ## 기여 방식 개요 -Delook 프로젝트에 새로운 기능이나 문서를 기여하려면 다음 절차를 따르면 됩니다: +현재 디룩 프로젝트는 다음 두 가지 방식의 기여를 받고 있습니다: + +1. **프로그래밍 관련 콘텐츠 기여** + + - 새로운 프로그래밍 개념이나 지식, 기술 면접 질의응답 콘텐츠 + - 기존 콘텐츠의 개선 및 보완 + +2. **버그 리포트 및 개선** + - 발견된 버그 신고 + - 버그 수정 및 개선 +> 기능 제안이나 UI 개선은 [이슈](https://github.com/delook-dev/delook/issues) 또는 [디스커션](https://github.com/delook-dev/delook/discussions)을 통해 남겨주시면 관리자가 검토 후 작업하도록 하겠습니다. + +기여를 진행하기 전에 다음 절차를 따라주세요: 1. GitHub 저장소를 자신의 계정으로 포크(fork)합니다. 포크한 저장소를 로컬에 클론(clone)하여 개발 환경을 준비합니다. 2. 작업 전 반드시 이슈(issue)를 생성합니다. 작업할 주제를 명확히 하고, 템플릿에 따라 작성해주세요. -3. main 브랜치에서 새로운 브랜치를 생성해 작업합니다. (예: `feat/bookmark`) - - - **문서(docs) 기여의 경우** - 작업 중인 브랜치가 다른 기여자의 브랜치와 충돌하지 않도록 - `docs/작성자명/주제` 형식으로 브랜치 이름을 명확하게 설정하는 것을 권장합니다. (예: `docs/waterbinnn/js-array`) +3. main 브랜치에서 새로운 브랜치를 생성해 작업합니다. - ※ 여러 개의 문서를 하나의 브랜치에서 작성하는 경우, 주제 부분에는 보다 포괄적인 범주 또는 작성 목적을 사용하는 것을 추천합니다. (예: `docs/waterbinnn/js`) + - **콘텐츠 기여의 경우**: `docs/작성자명/주제` 형식 (예: `docs/waterbinnn/js-array`) + - **버그 수정의 경우**: `fix/작성자명/버그-설명` 형식 (예: `fix/waterbinnn/scroll`) -4. 커밋 메시지 규칙을 지켜 커밋을 작성합니다. - 커밋 메시지는 아래 지침에 따라 작성합니다 (자세한 내용은 커밋 메시지 및 PR 규칙 섹션을 참고하세요). +4. 커밋 메시지 규칙을 지켜 커밋을 작성합니다. 5. 변경 사항을 푸시한 후 Pull Request(PR)를 생성합니다. @@ -40,9 +53,9 @@ Delook 프로젝트에 새로운 기능이나 문서를 기여하려면 다음 - 모든 작업은 먼저 이슈(issue)를 작성한 후 진행합니다. 반드시 이슈를 선행해주세요. - 작업에 맞는 이슈 템플릿을 선택해 작성해주세요. -- 문서(docs) 기여 시에는 가능하면 문서 1건당 이슈를 하나씩 작성해주세요. +- 콘텐츠(docs) 기여 시에는 가능하면 문서 1건당 이슈를 하나씩 작성해주세요. - `[DOCS]: 언어 또는 주제 카테고리 - 주제` - - 예: `[DOCS]: javascript - 콜백함수 개념 설명` + - 예: `[DOCS]: javascript - 콜백함수 개념 설명` - 여러 문서를 동시에 작성할 경우에도 각각 이슈를 남기고, PR은 하나로 묶어 제출 가능합니다. ## 커밋 메시지 및 PR 규칙 @@ -180,3 +193,67 @@ dateModified: 최종 수정일 (YYYY.MM.DD) - 예시는 ####(h4)로 구분합니다. ``` + +## 이미지 삽입 가이드 + +MDX 문서에 이미지를 삽입하려면 아래 절차를 따르세요. + +### 1. 이미지 파일 추가 + +이미지 파일을 아래 경로에 추가해주세요: + +- 경로: `public/docs-assets/${category}` +- `${category}`는 문서 주제에 맞게 설정 (예: `javascript`) +- 폴더가 없으면 직접 생성 +- **폴더명은 문서 카테고리와 일치해야 합니다.** + +> 예시: +> JavaScript 관련 문서인 경우 → +> `public/docs-assets/javascript/image.png` + +### 2. MDX 파일로 돌아가기 + +이미지를 삽입할 `.mdx` 파일로 돌아갑니다. + +### 3. 이미지 컴포넌트 import + +`---` 메타태그 블록 아래에 다음 import 문을 추가합니다. + +```jsx +import { ImageContainer } from '/src/components/ui/ImageContainer.tsx'; +``` + +### 4. 이미지 컴포넌트 사용 + +본문에 아래와 같이 컴포넌트를 사용하세요. + +```tsx + +``` + +props 설명: + +| 속성 | 설명 | +| ---------- | ------------------------------------- | +| `category` | `public/docs-assets` 하위 폴더명 | +| `fileName` | 이미지 파일명 (확장자 포함) | +| `alt` | 이미지 대체 텍스트 (접근성 향상 목적) | + +### 5. 전체 예시 코드 + +```mdx +--- +title: 이미지 삽입 테스트 +type: concept +language: Javascript +tags: + - Javascript +dateModified: 2025.00.00 +--- + +import { ImageContainer } from '/src/components/ui/ImageContainer.tsx'; + +# 이미지 삽입 예시 + + +``` diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..4409fb4 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,36 @@ +# 개인정보 처리방침 + +디룩(Delook)은 사용자의 개인정보 보호를 중요하게 생각하며, 사용자로부터 수집하는 정보의 최소화와 안전한 처리를 최우선으로 합니다. 본 방침은 서비스(확장프로그램, 웹사이트 등)를 이용할 경우 적용되며, 서비스 이용 시 아래 내용에 동의하는 것으로 간주합니다. + +## 1. 수집하는 정보 + +서비스 제공을 위해 다음과 같은 데이터를 수집할 수 있습니다: + +- 사용자가 설정한 테마 정보 +- 사용자가 저장한 북마크 정보 +- 콘텐츠 필터링 및 커스터마이징 관련 설정 + +모든 데이터는 사용자의 브라우저 로컬 스토리지에만 저장되며 외부 서버로 전송되지 않습니다. +단, Google Analytics를 통한 서비스 이용 통계 데이터는 Google의 서버로 전송될 수 있습니다. + +## 2. 정보의 사용 목적 + +- 사용자의 새 탭 환경을 개인화하여 콘텐츠를 보여줍니다. +- 사용자가 저장한 콘텐츠에 빠르게 접근할 수 있도록 합니다. +- 사용자가 설정한 필터나 테마 등 UI를 지속적으로 반영합니다. + +## 3. 외부 전송 및 제3자 제공 + +본 서비스는 서버와의 통신 없이 동작하며, 사용자의 어떠한 개인정보도 외부 서버로 전송하거나 제3자에게 제공하지 않습니다. + +## 4. 권한 사용 내역 + +- `storage`: 사용자 설정(테마, 필터, 북마크 등)을 브라우저에 저장하기 위해 필요합니다. 이 정보는 외부로 전송되지 않습니다. + +## 5. 원천 코드 및 오픈소스 + +디룩(Delook)은 오픈소스로, 누구나 코드를 확인하고 개선에 [기여](https://github.com/delook-dev/delook/blob/main/CONTRIBUTING.md)할 수 있습니다. 자세한 코드는 [GitHub 저장소](https://github.com/delook-dev/delook)에서 확인하실 수 있습니다: + +## 6. 사용자 권리 + +본 서비스는 사용자의 개인정보를 수집하지 않기 때문에, 별도의 개인정보 열람, 수정, 삭제 요청 절차는 존재하지 않습니다. 단, 사용자는 Chrome 브라우저의 확장 프로그램 관리 또는 로컬 스토리지 삭제를 통해 언제든지 데이터를 초기화할 수 있습니다. diff --git a/README.md b/README.md index 19f0123..7eed7a2 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,53 @@ -※ 현재 웹사이트는 배포 완료된 상태이며, 크롬 확장 프로그램은 아직 웹스토어에 등록되지 않았습니다. - # 디룩 (Delook) -> 성장하는 개발자를 위한 오픈소스 기반 지식 공유 플랫폼
+> 성장하는 개발자를 위한 오픈소스 기반 지식 공유 플랫폼

- - Delook 바로가기🎉 - + Delook OG Image Preview

- Delook OG Image Preview + + Delook 바로가기 +

-Delook은 매일 새로운 탭에서 프로그래밍 개념을 자연스럽게 학습할 수 있도록 돕는 크롬 확장 프로그램입니다. +디룩(Delook)은 브라우저 새 탭에서 프로그래밍 지식을 학습할 수 있도록 돕는 플랫폼입니다. 프로젝트가 마음에 드셨다면 `⭐️ star`로 응원해주세요! ---- - -## ✨ 기능 소개 +## 기능 소개 - 접속할 때마다 랜덤한 프로그래밍 개념을 학습할 수 있습니다. -- 카테고리나 제목으로 원하는 개념을 쉽게 찾을 수 있습니다. -- 전체 아카이브 또는 북마크를 통해 관심 있는 개념만 모아볼 수 있습니다. -- 개념 설명과 함께 예시를 확인할 수 있어 이해를 도와줍니다. +- 카테고리나 제목으로 원하는 개념을 쉽게 찾을 수 있습니다. +- 전체 아카이브 또는 북마크를 통해 관심 있는 개념만 모아볼 수 있습니다. +- 개념 설명과 함께 예시를 확인할 수 있어 이해를 도와줍니다. -## 🛠 기술 스택 +## 기술 스택 - **Frontend**: React 19, TypeScript, Vite - **Styling**: Tailwind CSS, shadcn/ui - **Platform**: Chrome Extension, Web - **Infra / Deployment**: AWS S3, CloudFront -## 🚀 설치 및 사용법 +## 배포 플로우 + +- `main` 브랜치에 병합 후 `production` 브랜치를 통해 배포가 진행됩니다. +- 웹사이트와 크롬 스토어 배포가 동시에 이루어지며, 크롬 스토어의 경우 심사 기간(약 2일)으로 인해 배포 시점에 차이가 있을 수 있습니다. + +## 설치 및 사용법 + +설치 및 사용법은 + + 여기에서 확인해주세요. + +## 기여 + +디룩은 함께 만들어가는 오픈소스 프로젝트입니다. +다음과 같은 기여를 받고 있습니다 -> 크롬 웹 스토어 등록 후 설치 안내 예정입니다. +- 콘텐츠 기여 +- 버그 리포트 및 개선 -## 🧠 기여 -디룩은 함께 만들어가는 오픈소스 프로젝트입니다.
지식을 나누고 싶은 누구나, Delook에 기여할 수 있습니다. +자세한 기여 방법은 [CONTRIBUTING.md](https://github.com/delook-dev/delook/blob/main/CONTRIBUTING.md)와 [CODE_OF_CONDUCT.md](https://github.com/delook-dev/delook/blob/main/CODE_OF_CONDUCT.md)를 참고해주세요. -> 기여를 원하신다면 [CONTRIBUTING.md](https://github.com/delook-dev/delook/blob/main/CONTRIBUTING.md)와 [CODE_OF_CONDUCT.md](https://github.com/delook-dev/delook/blob/main/CODE_OF_CONDUCT.md)를 참고해주세요. +기능 제안이나 UI 개선은 [Issues](https://github.com/delook-dev/delook/issues) 또는 [Discussions](https://github.com/delook-dev/delook/discussions)을 통해 남겨주시면 관리자가 검토 후 작업하도록 하겠습니다. diff --git a/docs/posts/fe-interview/brower-rendering.mdx b/docs/posts/fe-interview/brower-rendering.mdx new file mode 100644 index 0000000..8c84675 --- /dev/null +++ b/docs/posts/fe-interview/brower-rendering.mdx @@ -0,0 +1,29 @@ +--- +title: '브라우저 렌더링 과정은 어떻게 되나요?' +type: 'interview' +language: 'FE-Interview' +tags: + - Browser +dateModified: 2025.04.20 +--- + +{/* 📌 인터뷰 문서 */} + +## 답변 + +브라우저는 HTML과 CSS를 파싱해 화면에 표시되는 구조와 스타일을 계산한 뒤, 실제로 렌더링하는 과정을 거칩니다. + +먼저 HTML을 파싱해 `DOM 트리`를 만들고, CSS를 파싱해 `CSSOM 트리`를 생성합니다.
+이 두 트리를 결합해 `렌더 트리`를 구성한 뒤,
+각 요소의 위치와 크기를 계산하는 `레이아웃(Layout)` 단계를 수행합니다.
+이후 요소들을 픽셀로 칠하는 `페인트(Paint)` 과정을 거치고,
+마지막으로 여러 레이어를 조합해 최종 화면을 그리는 `합성(Composite)` 단계를 수행합니다. + +### Q. HTML 파싱 중에 ` + + diff --git a/public/images/devdevdev.png b/public/images/devdevdev.png new file mode 100644 index 0000000..5b30762 Binary files /dev/null and b/public/images/devdevdev.png differ diff --git a/public/images/og-image.png b/public/images/og-image.png new file mode 100644 index 0000000..3de78bc Binary files /dev/null and b/public/images/og-image.png differ diff --git a/public/images/og-thumbnail.png b/public/images/og-thumbnail.png deleted file mode 100644 index 0162e17..0000000 Binary files a/public/images/og-thumbnail.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index 9ea9a03..c99c14c 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,10 +3,10 @@ "name": "디룩(Delook)", "author": "@waterbinnn", "homepage_url": "https://www.delook.co.kr/", - "version": "1.0.0", - "description": "디룩은 새 탭을 열 때마다 랜덤한 프로그래밍 개념을 보여주어, 자연스럽게 지식을 확장할 수 있도록 돕는 확장 프로그램입니다. 꾸준한 학습을 통해 기술 면접과 실무에 자신감을 더하세요.", + "version": "1.0.5", + "description": "개념 학습부터 기술 면접 준비까지, 성장하는 개발자의 새 탭", "action": { - "default_popup": "index.html", + "default_popup": "popup.html", "default_icon": { "16": "icons/icon-16.png", "32": "icons/icon-32.png", @@ -29,6 +29,6 @@ }, "permissions": ["storage"], "content_security_policy": { - "extension_pages": "script-src 'self' https://delook.co.kr https://www.delook.co.kr; object-src 'self'" + "extension_pages": "script-src 'self'; object-src 'self'" } } diff --git a/public/sitemap.xml b/public/sitemap.xml index c3e5845..13a5720 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1 +1 @@ -https://www.delook.co.kr/2025-04-17T11:02:38.545Zdaily1.0https://www.delook.co.kr/about2025-04-17T11:02:38.545Zdaily1.0https://www.delook.co.kr/archive2025-04-17T11:02:38.545Zdaily1.0https://www.delook.co.kr/bookmark2025-04-17T11:02:38.545Zdaily1.0 \ No newline at end of file +https://www.delook.co.kr/2025-05-07T05:12:33.006Zdaily1.0https://www.delook.co.kr/about2025-05-07T05:12:33.006Zdaily1.0https://www.delook.co.kr/archive2025-05-07T05:12:33.006Zdaily1.0https://www.delook.co.kr/bookmark2025-05-07T05:12:33.006Zdaily1.0 \ No newline at end of file diff --git a/scripts/extension-version.js b/scripts/extension-version.js new file mode 100644 index 0000000..396fd77 --- /dev/null +++ b/scripts/extension-version.js @@ -0,0 +1,17 @@ +//익스텐션 버전 업그레이드 스크립트 +import fs from 'fs'; + +const manifestPath = 'public/manifest.json'; + +const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); + +const [major, minor, patch] = manifest.version.split('.').map(Number); +const nextPatch = patch + 1; +const today = new Date().toISOString().slice(0, 10).replace(/-/g, '.'); +const newVersion = `${major}.${minor}.${nextPatch}`; +const versionName = `${newVersion} (${today})`; + +manifest.version = newVersion; +manifest.version_name = versionName; + +fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); diff --git a/src/App.tsx b/src/App.tsx index 7084866..3dbf3ca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ -import { useRoutes } from 'react-router-dom'; +import { useEffect } from 'react'; +import { useLocation, useRoutes } from 'react-router-dom'; import { Layout } from '@/components'; import routes from '~react-pages'; @@ -6,6 +7,12 @@ import routes from '~react-pages'; import { ErrorPage } from './features/error'; export default function App() { + const location = useLocation(); + + useEffect(() => { + window.scrollTo(0, 0); + }, [location.pathname, location.search, location.hash]); + return ( <> {useRoutes([ diff --git a/src/components/layout/MetaTags.tsx b/src/components/layout/MetaTags.tsx index 32c8374..00c0a5f 100644 --- a/src/components/layout/MetaTags.tsx +++ b/src/components/layout/MetaTags.tsx @@ -1,8 +1,11 @@ -// components/SeoHead.tsx import { Helmet } from 'react-helmet-async'; import { SITE_URL } from '@/constants'; +const DEFAULT_DESCRIPTION = '개념 학습부터 기술 면접 준비까지, 성장하는 개발자의 새 탭'; +const DEFAULT_KEYWORDS = + '개발, 디룩, Delook, 개발 인사이트, 기술면접, CS 개념, 프론트엔드 학습, 백엔드 학습, 개발자 탭, 개발자 확장 프로그램'; + interface MetaTagsProps { title: string; description?: string; @@ -10,26 +13,28 @@ interface MetaTagsProps { keywords?: string; } -export const MetaTags = ({ title, description, url, keywords }: MetaTagsProps) => ( - - {title} - - - - - - - -); +export const MetaTags = ({ title, description, url, keywords }: MetaTagsProps) => { + const metaDescription = description ?? DEFAULT_DESCRIPTION; + const metaKeywords = keywords ? `${keywords}, ${DEFAULT_KEYWORDS}` : DEFAULT_KEYWORDS; + const metaUrl = url ?? SITE_URL; + + return ( + + {title} + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/layout/footer/Footer.tsx b/src/components/layout/footer/Footer.tsx index 9635a97..3527626 100644 --- a/src/components/layout/footer/Footer.tsx +++ b/src/components/layout/footer/Footer.tsx @@ -1,3 +1,5 @@ +import { Link } from 'react-router-dom'; + import { ExternalLink } from '@/components'; import { footerContents } from './_content'; @@ -8,7 +10,7 @@ export default function Footer() {
{footerContents.infos.map((info) => ( @@ -17,23 +19,31 @@ export default function Footer() { ))}
- +

{footerContents.intro} - +

-
- {footerContents.links.map((info) => ( - - {info.text} - - ))} +
+
+ {footerContents.outLinks.map((info) => ( + + {info.text} + + ))} + {footerContents.inLinks.map((info) => ( + + {info.text} + + ))} +
); diff --git a/src/components/layout/footer/_content.ts b/src/components/layout/footer/_content.ts index 7f127b4..63fea04 100644 --- a/src/components/layout/footer/_content.ts +++ b/src/components/layout/footer/_content.ts @@ -1,4 +1,4 @@ -import { CONTRIBUTE_SERVICE, GITHUB_URL, REPORT_BUGS } from '@/constants'; +import { CONTRIBUTE_SERVICE, GITHUB_URL, REPORT_BUGS, ROUTES } from '@/constants'; const footerContents = { infos: [ @@ -6,11 +6,12 @@ const footerContents = { { text: 'Made by @waterbinnn', href: 'https://github.com/waterbinnn' }, ], intro: `디룩은 오픈 소스 프로젝트입니다.\n여러분의 지식을 공유하고 함께 발전시켜 주세요!`, - links: [ + outLinks: [ { text: 'About', href: GITHUB_URL }, { text: 'Contribute', href: CONTRIBUTE_SERVICE }, - { text: 'Report Bugs', href: REPORT_BUGS }, + { text: 'Report_Bugs', href: REPORT_BUGS }, ], + inLinks: [{ text: 'Privacy', href: ROUTES.PRIVACY }], }; export { footerContents }; diff --git a/src/components/layout/header/FilterForm.tsx b/src/components/layout/header/FilterForm.tsx index a5c1757..55366a4 100644 --- a/src/components/layout/header/FilterForm.tsx +++ b/src/components/layout/header/FilterForm.tsx @@ -14,7 +14,7 @@ import { FormLabel, FormMessage, } from '@/components'; -import { toast, useFilterCategory } from '@/hooks'; +import { useFilterCategory } from '@/features/post'; const FormSchema = z.object({ items: z.array(z.string()).refine((value) => value.some((item) => item), { @@ -38,9 +38,6 @@ export function FilterForm() { const onSubmit = (data: z.infer) => { saveSettings(data.items); - toast({ - title: '필터 설정 완료! 🎉', - }); }; return ( diff --git a/src/components/ui/DevDevDev.tsx b/src/components/ui/DevDevDev.tsx new file mode 100644 index 0000000..acf8c65 --- /dev/null +++ b/src/components/ui/DevDevDev.tsx @@ -0,0 +1,28 @@ +import { ExternalLink } from './ExternalLink'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './Tooltip'; + +const DEVDEVDEV_URL = 'https://www.devdevdev.co.kr/techblog'; + +export const DevDevDev = () => { + return ( + + + + + 댑댑댑 + + + +
+ {`국내 빅테크 기업들의\n블로그 포스팅 보러가기`} + +
+
+
+
+ ); +}; diff --git a/src/components/ui/ImageContainer.tsx b/src/components/ui/ImageContainer.tsx new file mode 100644 index 0000000..8f54d3e --- /dev/null +++ b/src/components/ui/ImageContainer.tsx @@ -0,0 +1,17 @@ +interface ImageContainerProps { + category: string; + fileName: string; + alt: string; +} + +export const ImageContainer = ({ category, fileName, alt }: ImageContainerProps) => { + return ( +
+ {alt} +
+ ); +}; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index af56285..d75a4b7 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -2,9 +2,11 @@ export * from './Badge'; export * from './Button'; export * from './Checkbox'; export * from './Collapsible'; +export * from './DevDevDev'; export * from './ExternalLink'; export * from './Form'; export * from './IconButton'; +export * from './ImageContainer'; export * from './Input'; export * from './Label'; export * from './Popover'; diff --git a/src/constants/index.ts b/src/constants/index.ts index 15ced82..cdd3f68 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,4 @@ export * from './breakPoint'; export * from './routes'; export * from './storage'; +export * from './toastMsg'; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index c25a0de..fbecb04 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -9,11 +9,24 @@ const ROUTES = { BOOKMARK: `/bookmark`, ARCHIVE: `/archive`, ABOUT: `/about`, + PRIVACY: `/privacy`, } as const; //external links const GITHUB_URL = 'https://github.com/delook-dev'; -const CONTRIBUTE_SERVICE = `${GITHUB_URL}/delook/blob/main/CONTRIBUTING.md`; +const CONTRIBUTE_SERVICE = `${GITHUB_URL}/.github/blob/main/profile/README.md#%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC`; const REPORT_BUGS = `${GITHUB_URL}/delook/issues`; +const CHROME_STORE_URL = + 'https://chromewebstore.google.com/detail/ehfclaaaeofpkbgankkeokjgodoejahp'; +const DOWNLOAD_INFO_README = + 'https://github.com/delook-dev/.github/blob/main/profile/README.md#%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95'; -export { CONTRIBUTE_SERVICE, GITHUB_URL, REPORT_BUGS, ROUTES, SITE_URL }; +export { + CHROME_STORE_URL, + CONTRIBUTE_SERVICE, + DOWNLOAD_INFO_README, + GITHUB_URL, + REPORT_BUGS, + ROUTES, + SITE_URL, +}; diff --git a/src/constants/storage.ts b/src/constants/storage.ts index 06ca6be..b727fdd 100644 --- a/src/constants/storage.ts +++ b/src/constants/storage.ts @@ -1,6 +1,7 @@ const STORAGE_KEYS = { bookmark: 'saved_posts', filter: 'settings_category', + recentPosts: 'recent_posts', } as const; export { STORAGE_KEYS }; diff --git a/src/constants/toastMsg.ts b/src/constants/toastMsg.ts new file mode 100644 index 0000000..7814a77 --- /dev/null +++ b/src/constants/toastMsg.ts @@ -0,0 +1,7 @@ +export const ToastMsg = { + copyLink: '링크가 복사되었습니다 🎉', + filterSuccess: '필터가 설정되었습니다 🎉', + filterError: '이미 설정된 필터입니다 🫢', + addBookmark: '북마크에 저장되었습니다 🤗', + removeBookmark: '북마크에서 해제되었습니다 🫢', +} as const; diff --git a/src/features/bookmark/hooks/useBookmark.tsx b/src/features/bookmark/hooks/useBookmark.tsx index ea06e2d..53ef8ab 100644 --- a/src/features/bookmark/hooks/useBookmark.tsx +++ b/src/features/bookmark/hooks/useBookmark.tsx @@ -1,8 +1,7 @@ import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; import { useShallow } from 'zustand/shallow'; -import { ROUTES } from '@/constants'; +import { ToastMsg } from '@/constants'; import { PostMetaData, PostPathData } from '@/features/post'; import { toast } from '@/hooks'; @@ -15,9 +14,6 @@ export const useBookmark = ({ metaData: PostMetaData; pathData: PostPathData; }) => { - const navigate = useNavigate(); - const pathname = useLocation().pathname; - const [isBookmarked, setIsBookmarked] = useState(false); const { @@ -40,16 +36,12 @@ export const useBookmark = ({ if (isBookmarked) { removeBookmark({ ...pathData }); toast({ - title: '북마크 해제 완료! 🫢', + title: ToastMsg.removeBookmark, }); - - if (pathname.includes(ROUTES.BOOKMARK) && checkIsBookmarked(pathData) === false) { - navigate('/bookmark'); - } } else { addBookmark({ ...pathData, metaData }); toast({ - title: '북마크 완료! 나중에 꼭 다시 보기!! 🤗', + title: ToastMsg.addBookmark, }); } diff --git a/src/features/bookmark/store/useBookmarkStore.ts b/src/features/bookmark/store/useBookmarkStore.ts index ef46c64..7898f9b 100644 --- a/src/features/bookmark/store/useBookmarkStore.ts +++ b/src/features/bookmark/store/useBookmarkStore.ts @@ -12,7 +12,8 @@ import { PostPathData } from '@/features/post'; interface BookmarkStore { bookmarks: CategorizedBookmarks; // 북마크 데이터 - initializeBookmarks: () => Promise; // 북마크 초기화 + isError: boolean; + getBookmarks: () => Promise; // 북마크 초기화 addBookmark: (data: Omit) => Promise; // 북마크 추가 removeBookmark: (data: PostPathData) => Promise; // 북마크 삭제 isBookmarked: (data: PostPathData) => boolean; // 북마크 여부 확인 @@ -20,10 +21,19 @@ interface BookmarkStore { export const useBookmarkStore = create((set, get) => ({ bookmarks: {}, - - initializeBookmarks: async () => { - const bookmarks = await getStorageBookmarks(); - set({ bookmarks }); + isError: false, + + getBookmarks: async () => { + try { + set({ isError: false }); + const bookmarks = await getStorageBookmarks(); + set({ bookmarks }); + } catch (error) { + console.error('북마크 조회 오류:', error); + set({ + isError: true, + }); + } }, addBookmark: async (data) => { diff --git a/src/features/post/components/MDXHeader.tsx b/src/features/post/components/MDXHeader.tsx index dcdc96f..5a189d9 100644 --- a/src/features/post/components/MDXHeader.tsx +++ b/src/features/post/components/MDXHeader.tsx @@ -1,8 +1,11 @@ -import { Bookmark } from 'lucide-react'; +import { Bookmark, LucideShare2, RefreshCcw } from 'lucide-react'; +import { useLocation } from 'react-router-dom'; import { Badge, IconButton } from '@/components'; +import { SITE_URL, ToastMsg } from '@/constants'; import { useBookmark } from '@/features/bookmark'; -import { PostMetaData, PostPathData } from '@/features/post'; +import { PostMetaData, PostPathData, usePostStore } from '@/features/post'; +import { toast } from '@/hooks'; const ColorIconFilled = '#8c3fff'; @@ -13,11 +16,24 @@ export function MDXHeader({ metaData: PostMetaData; pathData: PostPathData; }) { + const pathname = useLocation().pathname; + + const { randomPost } = usePostStore(); + const { isBookmarked, handleBookmark } = useBookmark({ metaData, pathData, }); + const handleShare = () => { + const { category, filename } = pathData; + const url = `${SITE_URL}/archive?category=${category}&filename=${filename}`; + navigator.clipboard.writeText(url); + toast({ + title: ToastMsg.copyLink, + }); + }; + return (
@@ -28,17 +44,25 @@ export function MDXHeader({ > {metaData.language.toUpperCase()} - - {isBookmarked ? ( - - ) : ( - +
+ {pathname === '/' && ( + + + )} - + + + {isBookmarked ? ( + + ) : ( + + )} + + + + + +

{metaData.title}

diff --git a/src/features/post/hooks/useFilterCategory.ts b/src/features/post/hooks/useFilterCategory.ts new file mode 100644 index 0000000..de6125c --- /dev/null +++ b/src/features/post/hooks/useFilterCategory.ts @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { ToastMsg } from '@/constants'; +import { getPostsByCategory } from '@/features/post'; +import { useFilterStore } from '@/features/post/store/useFilterStore'; +import { toast } from '@/hooks'; + +type CategoryItem = { id: string; label: string }; +type SettingsMap = Record; + +export function useFilterCategory() { + const [categories, setCategories] = useState([]); + const [checked, setChecked] = useState([]); + + const { filter, setFilter } = useFilterStore(); + + const fetchCategories = useCallback(async () => { + const postsByCategory = await getPostsByCategory(); + const keys = Object.keys(postsByCategory); + + setCategories(keys.map((key) => ({ id: key, label: key }))); + + if (!filter) { + const initialFilter = Object.fromEntries(keys.map((key) => [key, true])) as SettingsMap; + await setFilter(initialFilter); + setChecked(keys); + } else { + setChecked(keys.filter((key) => filter[key])); + } + }, [filter, setFilter]); + + useEffect(() => { + fetchCategories(); + }, [fetchCategories]); + + const saveSettings = useCallback( + async (selectedCategories: string[]) => { + // 이전과 동일하면 저장하지 않음 + if (JSON.stringify(selectedCategories) === JSON.stringify(checked)) { + toast({ + title: ToastMsg.filterError, + }); + return; + } + + const newFilterSettings: SettingsMap = Object.fromEntries( + categories.map((c) => [c.id, selectedCategories.includes(c.id)]), + ); + + await setFilter(newFilterSettings); + setChecked(selectedCategories); + toast({ + title: ToastMsg.filterSuccess, + }); + }, + [categories, checked, setFilter], + ); + + return { + categories, + checked, + saveSettings, + }; +} diff --git a/src/features/post/hooks/usePostList.tsx b/src/features/post/hooks/usePostList.tsx index 3fb6480..1d55de2 100644 --- a/src/features/post/hooks/usePostList.tsx +++ b/src/features/post/hooks/usePostList.tsx @@ -29,27 +29,34 @@ export function usePostList({ const [postsByCategory, setPostsByCategory] = useState>({}); const [selectedPost, setSelectedPost] = useState(null); const [post, setPost] = useState(null); + const [isError, setIsError] = useState(false); useEffect(() => { const fetch = async () => { - const posts = await fetchPostList(); - setPostsByCategory(posts); + try { + const posts = await fetchPostList(); + setPostsByCategory(posts); - const firstCategory = Object.keys(posts)[0]; - const firstPost = posts[firstCategory]?.[0]; + const firstCategory = Object.keys(posts)[0]; + const firstPost = posts[firstCategory]?.[0]; - const selected = - category && filename - ? { category, filename } - : { category: firstCategory, filename: firstPost?.filename }; + const selected = + category && filename + ? { category, filename } + : { category: firstCategory, filename: firstPost?.filename }; - setSelectedPost(selected); + setSelectedPost(selected); - if (selected) { - const postData = await getPost(selected); - setPost(postData); + if (selected) { + const postData = await getPost(selected); + setPost(postData); + } + } catch (error) { + console.error('포스트 목록 조회 오류:', error); + setIsError(true); } }; + fetch(); }, [category, filename, fetchPostList]); @@ -63,5 +70,5 @@ export function usePostList({ })); }, [postsByCategory]); - return { postsByCategory, selectedPost, post, categoryList }; + return { postsByCategory, selectedPost, post, categoryList, isError }; } diff --git a/src/features/post/index.ts b/src/features/post/index.ts index 53dfec9..06a681d 100644 --- a/src/features/post/index.ts +++ b/src/features/post/index.ts @@ -2,6 +2,9 @@ export * from './components/MDXHeader'; export * from './components/MDXRender'; export * from './components/PostSidebarLayout'; export * from './components/RenderPost'; +export * from './hooks/useFilterCategory'; export * from './hooks/usePostList'; +export * from './store/useFilterStore'; +export * from './store/usePostStore'; export * from './types/postTypes'; export * from './utils/postUtils'; diff --git a/src/features/post/store/useFilterStore.ts b/src/features/post/store/useFilterStore.ts new file mode 100644 index 0000000..fd77856 --- /dev/null +++ b/src/features/post/store/useFilterStore.ts @@ -0,0 +1,20 @@ +import { create } from 'zustand'; + +import { STORAGE_KEYS } from '@/constants'; +import { saveToStorage } from '@/lib'; + +type FilteredCategory = Record; + +interface FilterStore { + filter: FilteredCategory | null; + setFilter: (filter: FilteredCategory) => Promise; +} + +export const useFilterStore = create((set) => ({ + filter: null, + + setFilter: async (filter) => { + await saveToStorage(STORAGE_KEYS['filter'], filter); + set({ filter }); + }, +})); diff --git a/src/features/post/store/usePostStore.ts b/src/features/post/store/usePostStore.ts new file mode 100644 index 0000000..3dd4000 --- /dev/null +++ b/src/features/post/store/usePostStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +import { getRandomPost as fetchRandomPost, PostData } from '@/features/post'; + +interface PostStore { + currentPost: PostData | null; + setCurrentPost: (post: PostData) => void; + randomPost: () => Promise; + isError: boolean; +} + +export const usePostStore = create((set) => ({ + currentPost: null, + isError: false, + setCurrentPost: (post) => set({ currentPost: post }), + randomPost: async () => { + try { + const post = await fetchRandomPost(); + set({ currentPost: post }); + } catch (error) { + console.error('랜덤 포스트 조회 오류:', error); + set({ isError: true }); + } + }, +})); diff --git a/src/features/post/utils/postUtils.ts b/src/features/post/utils/postUtils.ts index e9605dc..5dbedad 100644 --- a/src/features/post/utils/postUtils.ts +++ b/src/features/post/utils/postUtils.ts @@ -7,7 +7,7 @@ import { PostPathData, SinglePostViewData, } from '@/features/post'; -import { getFromStorage } from '@/lib'; +import { getFromStorage, saveToStorage } from '@/lib'; const postModules = import.meta.glob(`/docs/posts/**/*.mdx`, { eager: false, @@ -161,8 +161,14 @@ const getFilteredEntries = async (): Promise<[string, () => Promise => { const postPathList = await getFilteredEntries(); + const recentPosts = (await getFromStorage(STORAGE_KEYS.recentPosts)) || []; + + // 최근 본 포스트를 제외한 포스트 목록 + const availablePosts = postPathList.filter(([path]) => !recentPosts.includes(path)); + + const targetPosts = availablePosts.length > 0 ? availablePosts : postPathList; + const randomEntry = targetPosts[Math.floor(Math.random() * targetPosts.length)]; - const randomEntry = postPathList[Math.floor(Math.random() * postPathList.length)]; const [path, importer] = randomEntry; const post = await generatePostData(path, importer); @@ -171,6 +177,9 @@ const getRandomPost = async (): Promise => { throw new Error(`유효하지 않은 MDX 경로입니다: ${path}`); } + const updatedRecentPosts = [path, ...recentPosts].slice(0, 5); + await saveToStorage(STORAGE_KEYS.recentPosts, updatedRecentPosts); + return post; }; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 341f4ab..919f5c3 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,4 +1,3 @@ -export * from './useFilterCategory'; export * from './useMobile'; export * from './useTheme'; export * from './useToast'; diff --git a/src/hooks/useFilterCategory.ts b/src/hooks/useFilterCategory.ts deleted file mode 100644 index 68c884f..0000000 --- a/src/hooks/useFilterCategory.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { STORAGE_KEYS } from '@/constants'; -import { getPostsByCategory } from '@/features/post'; -import { getFromStorage, saveToStorage } from '@/lib'; - -const STORAGE_KEY = STORAGE_KEYS['filter']; - -type CategoryItem = { id: string; label: string }; -type SettingsMap = Record; - -export function useFilterCategory() { - const [categories, setCategories] = useState([]); - const [checked, setChecked] = useState([]); - - useEffect(() => { - const init = async () => { - const postsByCategory = await getPostsByCategory(); - const keys = Object.keys(postsByCategory); - - let settings = await getFromStorage(STORAGE_KEY); - - if (!settings) { - settings = Object.fromEntries(keys.map((key) => [key, true])) as SettingsMap; - await saveToStorage(STORAGE_KEY, settings); - } - - setCategories(keys.map((key) => ({ id: key, label: key }))); - setChecked(keys.filter((key) => settings?.[key])); - }; - - init(); - }, []); - - const saveSettings = async (nextChecked: string[]) => { - const next: FilteredCategory = Object.fromEntries( - categories.map((c) => [c.id, nextChecked.includes(c.id)]), - ); - await saveToStorage(STORAGE_KEY, next); - setChecked(nextChecked); - }; - - return { - categories, - checked, - saveSettings, - }; -} diff --git a/src/lib/storage.ts b/src/lib/storage.ts index d2360f4..7e2f214 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,8 +1,8 @@ //익스텐션 환경 여부 확인 -const isExtension = typeof chrome !== 'undefined' && !!chrome.storage; +export const isChromeExtension = typeof chrome !== 'undefined' && !!chrome.storage; /** - * Extenstion 스토리지 데이터를 반환 + * Extension 스토리지 데이터를 반환 * @param key 가져올 키 이름 * @returns StorageValueMap[K] 해당 키의 값 */ @@ -17,7 +17,7 @@ const getFromExtensionStorage = ( }; /** - * Extenstion 스토리지에 데이터를 저장 + * Extension 스토리지에 데이터를 저장 * @param key StorageKey * @param data StorageValueMap[K] * @returns boolean 성공여부 @@ -41,7 +41,7 @@ const saveToExtensionStorage = ( export const getFromStorage = async ( key: K, ): Promise => { - if (isExtension) { + if (isChromeExtension) { return await getFromExtensionStorage(key); } @@ -59,7 +59,7 @@ export const saveToStorage = async ( key: K, data: StorageValueMap[K], ): Promise => { - if (isExtension) { + if (isChromeExtension) { return await saveToExtensionStorage(key, data); } @@ -72,7 +72,7 @@ export const saveToStorage = async ( * @param key 제거할 키 이름 */ export const removeFromStorage = async (key: StorageKeyValue): Promise => { - if (isExtension) { + if (isChromeExtension) { return new Promise((resolve) => { chrome.storage.local.remove(key, () => resolve()); }); diff --git a/src/pages/about/_content.ts b/src/pages/about/_content.ts index 69f1a67..e6c98bd 100644 --- a/src/pages/about/_content.ts +++ b/src/pages/about/_content.ts @@ -12,14 +12,16 @@ const mainContents = `바쁘다 바빠 현대사회, 브라우저 탭만 열면 실무 & 면접에서 자주 쓰이는 개념을 자연스럽게 반복 학습할 수 있는 -공간을 제공합니다! -`; +공간을 제공합니다!`; const contributeContents = `디룩은 함께 만들어가는 오픈소스 프로젝트입니다. 배운 내용을 공유하고, -더 많은 개발자들과 함께 성장해보세요! -작은 기여도 환영합니다. -`; +더 많은 개발자들과 함께 성장해보세요!`; -export { contributeContents, mainContents }; +const crewContents = `현재 디룩의 콘텐츠를 채워줄 +1기 스터디원을 모집하고 있습니다! +함께 공부&기록하며 성장하고 싶은 개발자분들의 +많은 참여 부탁드립니다!`; + +export { contributeContents, crewContents, mainContents }; diff --git a/src/pages/about/index.tsx b/src/pages/about/index.tsx index 116be21..dad5b32 100644 --- a/src/pages/about/index.tsx +++ b/src/pages/about/index.tsx @@ -2,9 +2,11 @@ import { ArrowUpRight } from 'lucide-react'; import { MetaTags } from '@/components'; import { Button, ExternalLink } from '@/components/ui'; -import { CONTRIBUTE_SERVICE, SITE_URL } from '@/constants'; +import { CHROME_STORE_URL, CONTRIBUTE_SERVICE, DOWNLOAD_INFO_README, SITE_URL } from '@/constants'; -import { contributeContents, mainContents } from './_content'; +import { contributeContents, crewContents, mainContents } from './_content'; + +const isChrome = typeof window !== 'undefined' && /Chrome/.test(window.navigator.userAgent); export default function AboutPage() { return ( @@ -13,36 +15,78 @@ export default function AboutPage() {

디룩 | DeLook

-
-

디룩 소개글

-

- {mainContents} -

- -
- -
-

- {contributeContents} -

- -
+ +
+ + +
); } + +interface AboutSectionProps { + srTitle: string; + contents: string; + buttonText: string; + buttonHref: string; + buttonClassName?: string; + contentClassName?: string; + isContentHighlight?: boolean; +} + +function AboutSection({ + srTitle, + contents, + buttonText, + buttonHref, + buttonClassName = '', + contentClassName = '', +}: AboutSectionProps) { + const lines = contents.split('\n'); + + return ( +
+

{srTitle}

+
+ {lines.map((line, idx) => ( + + {line} +
+
+ ))} +
+ +
+ ); +} diff --git a/src/pages/archive/index.tsx b/src/pages/archive/index.tsx index 61a4470..a4ddf7c 100644 --- a/src/pages/archive/index.tsx +++ b/src/pages/archive/index.tsx @@ -11,11 +11,11 @@ export default function ArchivePage() { return posts; }, []); - const { categoryList, post, selectedPost } = usePostList({ fetchPostList }); + const { categoryList, post, selectedPost, isError } = usePostList({ fetchPostList }); + + if (!post || !selectedPost) return null; + if (isError) return ; - if (!post || !selectedPost) { - return ; - } const { metaData: { title }, } = post; @@ -25,7 +25,7 @@ export default function ArchivePage() { return ( <> ({ + bookmarks: state.bookmarks, + getBookmarks: state.getBookmarks, + isError: state.isError, + })), + ); useEffect(() => { - initializeBookmarks(); - }, [initializeBookmarks]); + getBookmarks(); + }, [getBookmarks]); const fetchPostList = useCallback(async () => { return bookmarks; @@ -22,31 +28,22 @@ export default function BookmarkPage() { fetchPostList, }); - if (categoryList.length === 0) { - return ; - } - - if (!post || !selectedPost) { - return ; - } - - const { - metaData: { title }, - } = post; + const Contents = () => { + if (categoryList.length === 0) return ; + if (isError) return ; + if (!post || !selectedPost) return null; - const { category, filename } = selectedPost; - - return ( - <> - + return ( + ); + }; + + return ( + <> + + {Contents()} ); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 00b47f7..2d6be3f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,21 +1,51 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; +import { useShallow } from 'zustand/shallow'; -import { MetaTags } from '@/components'; -import { getRandomPost, PostData, RenderPost } from '@/features/post'; +import { DevDevDev, MetaTags } from '@/components'; +import { ErrorPage } from '@/features/error'; +import { RenderPost, useFilterStore, usePostStore } from '@/features/post'; -export default function Home() { - const [post, setPost] = useState(null); +function PostContent() { + const { filter } = useFilterStore(); + + const { currentPost, randomPost, isError } = usePostStore( + useShallow((state) => ({ + currentPost: state.currentPost, + randomPost: state.randomPost, + isError: state.isError, + })), + ); useEffect(() => { - getRandomPost().then((v) => setPost(v)); - }, []); + randomPost(); + }, [randomPost]); + + useEffect(() => { + //현재 포스트의 카테고리가 선택한 카테고리에 포함되어 있지 않으면 랜덤 포스트 재생성 + if (filter && currentPost && !filter[currentPost.category]) { + randomPost(); + } + }, [filter, randomPost, currentPost]); - if (!post) return; + if (!currentPost) return null; + if (isError) return ; + + return ; +} + +export default function Home() { + useEffect(() => { + window.scrollTo({ + top: 0, + behavior: 'instant', + }); + }, []); return ( <> - + + ); } diff --git a/src/pages/privacy/index.tsx b/src/pages/privacy/index.tsx new file mode 100644 index 0000000..7de9ab7 --- /dev/null +++ b/src/pages/privacy/index.tsx @@ -0,0 +1,14 @@ +import PrivacyContent from '/PRIVACY.md'; +import { MetaTags } from '@/components'; +import { MDXRender } from '@/features/post'; + +export default function Privacy() { + return ( + <> + + + + + + ); +} diff --git a/src/popup.tsx b/src/popup.tsx new file mode 100644 index 0000000..5ce5a1e --- /dev/null +++ b/src/popup.tsx @@ -0,0 +1,21 @@ +import './styles/index.css'; + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +export const Popup = () => { + return ( +
+ delook-logo +

+ 앗, Delook은 새 탭으로만 이용할 수 있어요! +

+
+ ); +}; + +createRoot(document.getElementById('popup-root')!).render( + + + , +); diff --git a/src/styles/index.css b/src/styles/index.css index e8366a7..0764d39 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -11,6 +11,7 @@ html { scroll-behavior: smooth; + overflow-y: scroll; } body { diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 477ee3d..a5c40a6 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -10,5 +10,6 @@ declare global { type StorageValueMap = { saved_posts: CategorizedBookmarks; settings_category: FilteredCategory; + recent_posts: string[]; }; } diff --git a/vite.config.ts b/vite.config.ts index b342342..8a7731a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,5 @@ import Pages from 'vite-plugin-pages'; import { defineConfig } from 'vite'; -import generateSitemap from 'vite-plugin-pages-sitemap'; import mdx from '@mdx-js/rollup'; import prerender from '@prerenderer/rollup-plugin'; import react from '@vitejs/plugin-react'; @@ -14,8 +13,6 @@ import tsconfigPaths from 'vite-tsconfig-paths'; import { visualizer } from 'rollup-plugin-visualizer'; import { viteStaticCopy } from 'vite-plugin-static-copy'; -const isProd = process.env.NODE_ENV === 'production'; - // https://vite.dev/config/ export default defineConfig({ plugins: [ @@ -24,14 +21,6 @@ export default defineConfig({ svgr(), Pages({ dirs: 'src/pages', - onRoutesGenerated: () => { - if (isProd) { - return generateSitemap({ - routes: ['/', '/about', '/archive', '/bookmark'], - hostname: 'https://www.delook.co.kr/', - }); - } - }, }), viteStaticCopy({ targets: [ @@ -63,7 +52,7 @@ export default defineConfig({ postProcess(renderedRoute) { renderedRoute.html = renderedRoute.html .replace(/http:/i, 'https:') - .replace(/(https:\/\/)?(localhost|127\.0\.0\.1):\d*/i, 'https://delook.co.kr'); + .replace(/(https:\/\/)?(localhost|127\.0\.0\.1):\d*/i, '.'); }, }), ], @@ -72,6 +61,7 @@ export default defineConfig({ rollupOptions: { input: { main: './index.html', + popup: './popup.html', }, output: { manualChunks: { diff --git a/yarn.lock b/yarn.lock index 675eef7..c47418e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1483,11 +1483,6 @@ dependencies: undici-types "~6.21.0" -"@types/node@^17.0.5": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - "@types/react-dom@^19.0.4": version "19.1.2" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.2.tgz#bd1fe3b8c28a3a2e942f85314dcfb71f531a242f" @@ -1500,13 +1495,6 @@ dependencies: csstype "^3.0.2" -"@types/sax@^1.2.1": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.7.tgz#ba5fe7df9aa9c89b6dff7688a19023dd2963091d" - integrity sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A== - dependencies: - "@types/node" "*" - "@types/unist@*", "@types/unist@^3.0.0": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" @@ -1810,7 +1798,7 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -arg@^5.0.0, arg@^5.0.2: +arg@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== @@ -5520,11 +5508,6 @@ safe-buffer@5.2.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - scheduler@^0.26.0: version "0.26.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" @@ -5661,16 +5644,6 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -sitemap@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-8.0.0.tgz#eb6ea48f95787cd680b83683c555d6f6b5a903fd" - integrity sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - slice-ansi@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" @@ -6286,14 +6259,6 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -vite-plugin-pages-sitemap@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/vite-plugin-pages-sitemap/-/vite-plugin-pages-sitemap-1.7.1.tgz#03dbe17e03104d0f719549ebd8393f0ca650cbaf" - integrity sha512-XtrMxDTECbEGMXWPB22I+cUB7aij6rQvVq4/q6vqCfZ34IdUyVwMexCYrcvrEBuOZF0knivfLfJUDm45mgJHWg== - dependencies: - sitemap "^8.0.0" - xml-formatter "^3.6.2" - vite-plugin-pages@^0.33.0: version "0.33.0" resolved "https://registry.yarnpkg.com/vite-plugin-pages/-/vite-plugin-pages-0.33.0.tgz#fbf254d56a67a1ff02ed7ca1c6228214de68c799" @@ -6408,18 +6373,6 @@ ws@^8.18.1: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== -xml-formatter@^3.6.2: - version "3.6.5" - resolved "https://registry.yarnpkg.com/xml-formatter/-/xml-formatter-3.6.5.tgz#ef541394aa56de432ce5ead3f175640d8cd8a92e" - integrity sha512-5Dvux87y+abquO3Om8zRyOUdYkc22BnSS3zMhL2UgeCC+3lz9FbSBpAhzxmk+/qfTO3ypLRwTxJvByoG+FjTMA== - dependencies: - xml-parser-xo "^4.1.2" - -xml-parser-xo@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/xml-parser-xo/-/xml-parser-xo-4.1.3.tgz#b6f287a7de5b42b2b5a90be970fba296d87f828e" - integrity sha512-U6eN5Pyrlek9ottHVpT9e8YUax75oVYXbnYxU+utzDC7i+OyWj9ynsNMiZNQZvpuazbG0O7iLAs9FkcFmzlgSA== - xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"