diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml
index 5d94b43a..34de5077 100644
--- a/.github/workflows/production.yaml
+++ b/.github/workflows/production.yaml
@@ -5,53 +5,64 @@ on:
types:
- released
+env:
+ REGISTRY: ghcr.io
+ REPOSITORY: ${{ github.repository }}
+ IMAGE_TAG: prod${{ github.run_number }}
+
jobs:
build:
runs-on: ubuntu-latest
+
permissions:
+ contents: read
+ packages: write
deployments: write
+
environment:
name: production
url: https://ziggle.gistory.me
- outputs:
- ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
- ECR_REPOSITORY: ziggle-frontend
- IMAGE_TAG: prod${{ github.run_number }}
- steps:
- - uses: actions/checkout@v4
-
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v4
- with:
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws-region: ap-northeast-2
-
- - name: Login to Amazon ECR
- id: login-ecr
- uses: aws-actions/amazon-ecr-login@v2
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
- name: setup environment
run: |
echo "${{ vars.ENV }}" >> .env.production
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env.production
echo "IDP_CLIENT_SECRET=${{ secrets.IDP_CLIENT_SECRET }}" >> .env.production
+ echo "NEXT_PUBLIC_GA_TRACKING_ID=${{ secrets.GA_TRACKING_ID }}" >> .env.production
+ echo "NEXT_PUBLIC_AMPLITUDE_API_KEY=${{ secrets.AMPLITUDE_API_KEY }}" >> .env.production
- - name: Build, tag, and push image to Amazon ECR
- uses: docker/build-push-action@v5
- env:
- ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
- ECR_REPOSITORY: ziggle-frontend
- IMAGE_TAG: prod${{ github.run_number }} # Use run number as image tag
+ - name: Login to the Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) from the Docker
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
+ tags: ${{ env.IMAGE_TAG }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push Docker image
+ id: push
+ uses: docker/build-push-action@v6
with:
context: .
push: true
- tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+
deploy:
name: Update Git Repository
needs: build
@@ -65,19 +76,12 @@ jobs:
fetch-depth: 0
- name: Update Kubernetes Manifest
- env:
- ECR_REGISTRY: ${{ needs.build.outputs.ECR_REGISTRY }}
- ECR_REPOSITORY: ${{ needs.build.outputs.ECR_REPOSITORY }}
- IMAGE_TAG: ${{ needs.build.outputs.IMAGE_TAG }}
run: |
- sed -i "s|image:.*|image: $ECR_REGISTRY\/$ECR_REPOSITORY:$IMAGE_TAG|g" infoteam/service/ziggle/next.prod.yaml
+ sed -i "s|image:.*|image: $REGISTRY\/$REPOSITORY:$IMAGE_TAG|g" infoteam/service/ziggle/next.prod.yaml
- name: Commit and Push
- env:
- ECR_REPOSITORY: ${{ needs.build.outputs.ECR_REPOSITORY }}
- IMAGE_TAG: ${{ needs.build.outputs.IMAGE_TAG }}
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "GitHub Actions"
- git commit -am "Update image $ECR_REPOSITORY:$IMAGE_TAG"
+ git commit -am "Update image $REPOSITORY:$IMAGE_TAG"
git push -u origin master
diff --git a/.github/workflows/staging.yaml b/.github/workflows/staging.yaml
index 6c249d5e..1fa62521 100644
--- a/.github/workflows/staging.yaml
+++ b/.github/workflows/staging.yaml
@@ -5,34 +5,28 @@ on:
branches:
- master
+env:
+ REGISTRY: ghcr.io
+ REPOSITORY: ${{ github.repository }}
+ IMAGE_TAG: dev${{ github.run_number }}
+
jobs:
build:
+ name: Build docker image
runs-on: ubuntu-latest
+
permissions:
+ contents: read
+ packages: write
deployments: write
+
environment:
name: staging
url: https://stg.ziggle.gistory.me
- outputs:
- ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
- ECR_REPOSITORY: ziggle-frontend
- IMAGE_TAG: dev${{ github.run_number }}
- steps:
- - uses: actions/checkout@v4
-
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v4
- with:
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws-region: ap-northeast-2
- - name: Login to Amazon ECR
- id: login-ecr
- uses: aws-actions/amazon-ecr-login@v2
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
- name: setup environment
run: |
@@ -40,18 +34,34 @@ jobs:
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env.production
echo "IDP_CLIENT_SECRET=${{ secrets.IDP_CLIENT_SECRET }}" >> .env.production
- - name: Build, tag, and push image to Amazon ECR
- uses: docker/build-push-action@v5
- env:
- ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
- ECR_REPOSITORY: ziggle-frontend
- IMAGE_TAG: dev${{ github.run_number }} # Use run number as image tag
+ - name: Login to the Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) from the Docker
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
+ tags: ${{ env.IMAGE_TAG }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push Docker image
+ id: push
+ uses: docker/build-push-action@v6
with:
context: .
push: true
- tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+
deploy:
name: Update Git Repository
needs: build
@@ -65,19 +75,12 @@ jobs:
fetch-depth: 0
- name: Update Kubernetes Manifest
- env:
- ECR_REGISTRY: ${{ needs.build.outputs.ECR_REGISTRY }}
- ECR_REPOSITORY: ${{ needs.build.outputs.ECR_REPOSITORY }}
- IMAGE_TAG: ${{ needs.build.outputs.IMAGE_TAG }}
run: |
- sed -i "s|image:.*|image: $ECR_REGISTRY\/$ECR_REPOSITORY:$IMAGE_TAG|g" infoteam/service/ziggle/next.stg.yaml
+ sed -i "s|image:.*|image: $REGISTRY\/$REPOSITORY:$IMAGE_TAG|g" infoteam/service/ziggle/next.stg.yaml
- name: Commit and Push
- env:
- ECR_REPOSITORY: ${{ needs.build.outputs.ECR_REPOSITORY }}
- IMAGE_TAG: ${{ needs.build.outputs.IMAGE_TAG }}
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "GitHub Actions"
- git commit -am "Update image $ECR_REPOSITORY:$IMAGE_TAG"
+ git commit -am "Update image $REPOSITORY:$IMAGE_TAG"
git push -u origin master
diff --git a/.gitignore b/.gitignore
index 32b7aa3c..1da1ebdb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,4 +43,7 @@ next-env.d.ts
/playwright-report/
/blob-report/
/playwright/.cache/
-playwright/.auth
\ No newline at end of file
+playwright/.auth
+playwright
+test-results
+playwright-report
\ No newline at end of file
diff --git a/.storybook/main.ts b/.storybook/main.ts
index 80f9a297..4c173744 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -1,6 +1,7 @@
-import type { StorybookConfig } from '@storybook/nextjs';
import { dirname, join, resolve } from 'path';
+import type { StorybookConfig } from '@storybook/nextjs';
+
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
@@ -18,6 +19,10 @@ const config: StorybookConfig = {
getAbsolutePath('@storybook/addon-postcss'),
],
+ features: {
+ experimentalRSC: true,
+ },
+
framework: {
name: getAbsolutePath('@storybook/nextjs'),
options: {
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7962fd64..f3bfe664 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -14,6 +14,7 @@
},
"editor.formatOnSave": false,
"cSpell.words": [
+ "Swal",
"Zabo",
"Ziggle"
],
diff --git a/README.md b/README.md
index 41b06d86..cdb2f8bd 100644
--- a/README.md
+++ b/README.md
@@ -18,18 +18,17 @@
│ └── tag
├── app
│ ├── [lng]: 지글의 모든 화면은 이 디렉토리 밑에 있습니다
- │ │ ├── (common)
- │ │ │ ├── (common)
- │ │ │ │ ├── mypage
- │ │ │ │ ├── search
- │ │ │ │ └── section
- │ │ │ │ └── [type]
- │ │ │ └── (needSidebar)
- │ │ │ ├── [category]
- │ │ │ └── notice
- │ │ │ └── [id]
- │ │ │ └── assets
- │ │ ├── (empty): 레이아웃이 비어있는 화면
+ │ │ ├── (group): ziggle groups 관련 (분리 후 제거 예정)
+ │ │ │ └── group
+ │ │ ├── (with-page-layout): Header, Footer, Toast Layout이 있는 페이지들
+ │ │ │ ├── (with-sidebar-layout): Sidebar가 있는 페이지들
+ │ │ │ │ ├── [category]
+ │ │ │ │ └── notice
+ │ │ │ │ └── [id]
+ │ │ │ └── (without-sidebar-layout): Sidebar가 없는 페이지들
+ │ │ │ ├── mypage
+ │ │ │ └── search
+ │ │ ├── (without-page-layout): Header, Footer, Toast Layout이 없는 페이지들
│ │ │ ├── app: /[lng]/app - 앱 설치 페이지 리다이렉션
│ │ │ └── login: /[lng]/login - 로그인 페이지 리다이렉션
│ │ └── (write): 사이드바가 없고, 상단바가 있는 글쓰기 화면
@@ -45,36 +44,25 @@
│ │ │ │ └── foreign
│ │ │ ├── additional
│ │ │ └── full
- │ │ └── og: open graph image를 생성하는 api
- │ ├── components: 모든 컴포넌트는 이 디렉토리 밑에 있습니다
- │ │ ├── atoms: 가장 작은 단위의 컴포넌트
- │ │ │ ├── Analytics
- │ │ │ ├── Button
- │ │ │ ├── Checkbox
- │ │ │ ├── ExternalLink
- │ │ │ └── Toggle
- │ │ ├── molecules: atom을 조합한 컴포넌트
- │ │ │ ├── Chip
- │ │ │ ├── DDay
- │ │ │ ├── HighlightedText
- │ │ │ ├── HorizontalScrollButton
- │ │ │ ├── Pagination
- │ │ │ ├── Tag
- │ │ │ └── ZaboImage
- │ │ ├── organisms: molecule을 조합한 컴포넌트
- │ │ │ ├── DateTimePicker
- │ │ │ ├── ImageCarousel
- │ │ │ ├── Tags
- │ │ │ └── Zabo
- │ │ └── templates: organism을 조합한 컴포넌트
- │ │ ├── Footer
+ │ │ ├── og: open graph image를 생성하는 api
+ │ │ ├── vapor-bff
+ │ │ │ └── [...ziggle]
+ │ │ └── ziggle
+ │ │ └── [...proxy]
+ │ ├── components: 여러 곳에서 공통적으로 쓰이는 컴포넌트
+ │ │ ├── layout: 레이아웃에 쓰이는 컴포넌트
+ │ │ │ ├── Footer
+ │ │ │ ├── Navbar
+ │ │ │ ├── NavbarWrite
+ │ │ │ └── Sidebar
+ │ │ └── shared: 레이아웃에 쓰이지 않고 공통적으로 쓰이는 컴포넌트
+ │ │ ├── Analytics
+ │ │ ├── Button
│ │ ├── LoadingCatAnimation
- │ │ ├── Navbar
- │ │ ├── NavbarWrite
- │ │ ├── ResultZabo
- │ │ ├── SearchAnimation
- │ │ ├── SearchResults
- │ │ └── Sidebar
+ │ │ ├── Pagination
+ │ │ ├── Tags
+ │ │ ├── Toggle
+ │ │ └── Zabo
│ └── i18next: 다국어 지원을 위한 설정
│ └── locales
│ ├── en
@@ -84,8 +72,7 @@
│ ├── fonts
│ ├── icons
│ └── logos
- ├── mock: 개발 환경(storybook)에서 사용하는 mock 데이터
- └── utils: 유틸성 파일
+ └── mock: 개발 환경(storybook)에서 사용하는 mock 데이터
```
### 몇가지 알아두면 좋은 점
diff --git a/package.json b/package.json
index 7e07e4be..e9bafe73 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,8 @@
"test:watch": "jest --watch"
},
"dependencies": {
+ "@amplitude/analytics-browser": "^2.11.9",
+ "@next/third-parties": "^15.0.3",
"@tinymce/tinymce-react": "^4.3.0",
"@toss/use-overlay": "^1.4.0",
"@vercel/og": "^0.5.17",
@@ -28,6 +30,7 @@
"lottie-react": "^2.4.0",
"next": "14.2.3",
"next-auth": "^4.24.7",
+ "next-themes": "^0.4.3",
"react": "^18",
"react-calendar": "^4.6.1",
"react-clock": "^4.5.1",
diff --git a/src/api/log/log-events.ts b/src/api/log/log-events.ts
index 78effc06..9bd947b5 100644
--- a/src/api/log/log-events.ts
+++ b/src/api/log/log-events.ts
@@ -2,45 +2,76 @@ const LogEvents = {
screenView: 'screen_view',
// Nav Bar
- navBarClickLogo: 'nav_bar_click_logo',
- navBarClickAll: 'nav_bar_click_all',
- navBarClickWrite: 'nav_bar_click_write',
- navBarClickSearch: 'nav_bar_click_search',
- navBarClickLogin: 'nav_bar_click_login',
- navBarClickMyPage: 'nav_bar_click_my_page',
+ navBarClickLogo: 'navbar_click_logo',
+ navBarClickLogin: 'navbar_click_login',
+ navBarClickMyPage: 'navbar_click_my_page',
+ // only for mobile
+ navBarClickMenu: 'navbar_click_menu',
+
+ // Search Page
+ searchSubmit: 'search_submit',
+ searchClickSearch: 'search_click_search',
+ searchChangeKeyword: 'search_change_keyword',
+ searchClickClear: 'search_click_clear',
+ // only for mobile
+ searchClickExpand: 'search_click_expand',
+ searchClickCancel: 'search_click_cancel',
// Footer
footerClickGithub: 'footer_click_github',
footerClickPlayStore: 'footer_click_play_store',
footerClickAppStore: 'footer_click_app_store',
footerClickInfo: 'footer_click_info',
+ footerClickBugReport: 'footer_click_bug_report',
footerClickServiceTerms: 'footer_click_service_terms',
footerClickPrivacyPolicy: 'footer_click_privacy_policy',
footerClickContact: 'footer_click_contact',
+ footerClickHouse: 'footer_click_house',
footerClickGist: 'footer_click_gist',
footerClickGijol: 'footer_click_gijol',
- // Home Page
+ // Sidebar
+ sidebarClickLink: 'sidebar_click_link',
+ sidebarClickProfile: 'sidebar_click_profile',
- // Search Page
- searchPageSubmit: 'search_page_submit',
- searchPageTypeChange: 'search_page_type_change',
- searchPageClickCancel: 'search_page_click_cancel',
+ // My Page
+ myClickMyNotice: 'my_click_my_notice',
+ myClickReminded: 'my_click_reminded',
+ myClickBugReport: 'my_click_bug_report',
+ myToggleLanguage: 'my_toggle_language',
+ myClickMode: 'my_click_mode',
+ myClickLogout: 'my_click_logout',
+ myClickUnregister: 'my_click_unregister',
+
+ // Notice Detail Page
+ detailClickImage: 'detail_click_image',
+ detailClickReaction: 'detail_click_reaction',
+ detailClickShare: 'detail_click_share',
+ detailClickCopyLink: 'detail_click_copy_link',
+ // only for author
+ detailClickEdit: 'detail_click_edit',
+ detailClickRemove: 'detail_click_remove',
// Notice Writing Page
- noticeWritingPageTypeTitle: 'notice_writing_page_type_title',
- noticeWritingPageCheckDeadline: 'notice_writing_page_check_deadline',
- noticeWritingPageCheckEnglish: 'notice_writing_page_check_english',
- noticeWritingPageSetDeadline: 'notice_writing_page_set_deadline',
- noticeWritingPageSetType: 'notice_writing_page_set_type',
- noticeWritingPageTypeTag: 'notice_writing_page_type_tag',
- noticeWritingPageTypeContent: 'notice_writing_page_type_content',
- noticeWritingPageClickSubmit: 'notice_writing_page_click_submit',
+ writingAcceptSaved: 'writing_accept_saved',
+ writingRejectSaved: 'writing_reject_saved',
+ writingToggleEnglish: 'writing_toggle_english',
+ writingChangeTab: 'writing_change_tab',
+ writingClickDeepl: 'writing_click_deepl',
+ writingSelectType: 'writing_select_type',
+ writingToggleDeadline: 'writing_toggle_deadline',
+ writingSetDeadline: 'writing_set_deadline',
+ writingSubmit: 'writing_submit',
+ // edit mode
+ writingModify: 'writing_modify',
+
+ // Category Page
+ categoryToggleDeadline: 'category_toggle_deadline',
// Components
noticeClick: 'notice_click',
- searchResultClick: 'search_result_click',
- search: 'search',
+ noticeClickReaction: 'notice_click_reaction',
+ noticeClickShare: 'notice_click_share',
} as const;
export default LogEvents;
diff --git a/src/api/log/send-log.ts b/src/api/log/send-log.ts
index bb0799d3..4b8af292 100644
--- a/src/api/log/send-log.ts
+++ b/src/api/log/send-log.ts
@@ -1,3 +1,6 @@
+import { track } from '@amplitude/analytics-browser';
+import { sendGAEvent } from '@next/third-parties/google';
+
import LogEvents from './log-events';
declare global {
@@ -6,22 +9,21 @@ declare global {
}
}
-export const analytics = Object.entries(LogEvents).reduce(
- (acc, [key, value]) => ({
- ...acc,
- [`log${key.charAt(0).toUpperCase() + key.slice(1)}`]: (
- properties?: object,
- ) => sendLog(value, properties),
- }),
- {},
-) as Record<
- `log${Capitalize}`,
- (properties?: object) => void
->;
-
const sendLog = (
event: (typeof LogEvents)[keyof typeof LogEvents],
properties?: object,
-) => window.smartlook('track', event, properties);
+) => {
+ if (process.env.NODE_ENV !== 'production') {
+ return;
+ }
+
+ window.smartlook('track', event, properties);
+
+ properties
+ ? sendGAEvent('event', event, properties)
+ : sendGAEvent('event', event);
+
+ track(event, properties);
+};
export default sendLog;
diff --git a/src/api/notice/notice.ts b/src/api/notice/notice.ts
index 57076af4..63c45c45 100644
--- a/src/api/notice/notice.ts
+++ b/src/api/notice/notice.ts
@@ -44,6 +44,7 @@ export interface Notice {
uuid: string;
};
createdAt: dayjs.Dayjs | string;
+ publishedAt: dayjs.Dayjs | string;
tags: string[];
views: number;
imageUrls: string[];
diff --git a/src/api/notice/send-alarm.ts b/src/api/notice/send-alarm.ts
new file mode 100644
index 00000000..43a0a785
--- /dev/null
+++ b/src/api/notice/send-alarm.ts
@@ -0,0 +1,5 @@
+import { ziggleApi } from '..';
+import type { Notice } from './notice';
+
+export const sendNoticeAlarm = ({ id }: Pick) =>
+ ziggleApi.post(`/notice/${id}/alarm`).then((res) => res.data);
diff --git a/src/api/tag/tag.ts b/src/api/tag/tag.ts
index 891fe520..ab8a1847 100644
--- a/src/api/tag/tag.ts
+++ b/src/api/tag/tag.ts
@@ -1,4 +1,4 @@
-import { Tag } from '@/app/[lng]/(write)/write/TagInput';
+import { Tag } from '@/app/[lng]/write/TagInput';
import { ziggleApi } from '..';
diff --git a/src/app/[lng]/(common)/(common)/layout.tsx b/src/app/[lng]/(common)/(common)/layout.tsx
deleted file mode 100644
index c837aeba..00000000
--- a/src/app/[lng]/(common)/(common)/layout.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import Sidebar from '@/app/components/templates/Sidebar';
-import { createTranslation, PropsWithLng } from '@/app/i18next';
-
-export default async function Layout({
- children,
- params: { lng },
-}: {
- children: React.ReactNode;
- params: PropsWithLng;
-}) {
- const { t } = await createTranslation(lng);
-
- return {children}
;
-}
diff --git a/src/app/[lng]/(common)/(common)/mypage/ChangeDarkModeBox.tsx b/src/app/[lng]/(common)/(common)/mypage/ChangeDarkModeBox.tsx
deleted file mode 100644
index fc6bd9db..00000000
--- a/src/app/[lng]/(common)/(common)/mypage/ChangeDarkModeBox.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-import { useCookies } from 'react-cookie';
-
-import Toggle from '@/app/components/atoms/Toggle/Toggle';
-import { PropsWithLng } from '@/app/i18next';
-import { useTranslation } from '@/app/i18next/client';
-
-import MypageBox from './MypageBox';
-
-export type ColorTheme = 'light' | 'dark';
-export type ColorThemeCookie = { name: 'theme'; value: ColorTheme };
-
-export const useColorTheme = (): [
- ColorTheme | undefined,
- (newTheme: ColorTheme) => void,
-] => {
- const [cookies, setCookie] = useCookies(['theme']);
- const [theme, setTheme] = useState();
-
- useEffect(() => {
- // First, try to get the theme from cookies
- const cookieTheme = cookies.theme as ColorTheme | undefined;
-
- if (cookieTheme) {
- setTheme(cookieTheme);
- } else {
- // If no cookie is set, check for system preference
- const prefersDarkMode =
- window.matchMedia &&
- window.matchMedia('(prefers-color-scheme: dark)').matches;
- const systemTheme: ColorTheme = prefersDarkMode ? 'dark' : 'light';
- setTheme(systemTheme);
- setCookie('theme', systemTheme, { path: '/' });
- }
- // console.log(theme, cookies.theme);
- }, [theme, cookies.theme, setCookie]);
-
- const updateTheme = (newTheme: ColorTheme) => {
- setTheme(newTheme);
- setCookie('theme', newTheme, { path: '/' });
- };
-
- return [theme, updateTheme];
-};
-
-const ChangeDarkModeBox = ({
- lng,
- defaultTheme,
-}: PropsWithLng<{ defaultTheme: ColorTheme }>) => {
- const { t } = useTranslation(lng);
-
- const [theme, setTheme] = useColorTheme();
-
- return (
-
-
-
- {t('mypage.switchDarkMode')}
-
-
{
- setTheme(theme === 'dark' ? 'light' : 'dark');
- window.location.reload();
- }}
- />
-
-
- );
-};
-
-export default ChangeDarkModeBox;
diff --git a/src/app/[lng]/(common)/(common)/mypage/ClientActions.tsx b/src/app/[lng]/(common)/(common)/mypage/ClientActions.tsx
deleted file mode 100644
index 6715dafd..00000000
--- a/src/app/[lng]/(common)/(common)/mypage/ClientActions.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-'use client';
-
-import { signOut } from 'next-auth/react';
-
-import Button from '@/app/components/atoms/Button';
-import { PropsWithLng } from '@/app/i18next';
-import { useTranslation } from '@/app/i18next/client';
-
-import MypageBox from './MypageBox';
-
-export default function ClientActions({ lng }: PropsWithLng) {
- const { t } = useTranslation(lng);
-
- const handleSignOut = () => {
- signOut({ callbackUrl: '/' });
- };
-
- const handleWithdrawal = () => {
- window.open(process.env.NEXT_PUBLIC_IDP_BASE_URL);
- };
-
- return (
- <>
-
-
-
- >
- );
-}
diff --git a/src/app/[lng]/(common)/(common)/section/[type]/layout.tsx b/src/app/[lng]/(common)/(common)/section/[type]/layout.tsx
deleted file mode 100644
index 7e8179e5..00000000
--- a/src/app/[lng]/(common)/(common)/section/[type]/layout.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { createTranslation, PropsWithLng } from '@/app/i18next';
-
-const AllNoticeLayout = async ({
- params: { lng, type },
- children,
-}: {
- params: PropsWithLng<{ type: 'all' | 'urgent' }>;
- children: React.ReactNode;
-}) => {
- const { t } = await createTranslation(lng);
-
- return (
-
-
-
- {t(`notices.${type === 'urgent' ? 'deadline' : type}.label`)}
-
-
- {t(`notices.${type === 'urgent' ? 'deadline' : type}.description`)}
-
-
- {children}
-
- );
-};
-
-export default AllNoticeLayout;
diff --git a/src/app/[lng]/(common)/(common)/section/[type]/page.tsx b/src/app/[lng]/(common)/(common)/section/[type]/page.tsx
deleted file mode 100644
index e233eff1..00000000
--- a/src/app/[lng]/(common)/(common)/section/[type]/page.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { notFound } from 'next/navigation';
-
-import SearchResults from '@/app/components/templates/SearchResults';
-import { PropsWithLng } from '@/app/i18next';
-
-export const dynamic = 'force-dynamic';
-
-const tags = ['event', 'recruit', 'general', 'academic'];
-const types = ['all', 'urgent', 'hot', ...tags, 'written', 'reminded'];
-
-const AllNoticePage = async ({
- searchParams,
- params: { lng, type },
-}: {
- params: PropsWithLng<{ type: string }>;
- searchParams: { page: string };
-}) => {
- if (!types.includes(type)) notFound();
- const { page } = searchParams;
- const pageNumber = Number(page) || 0;
-
- return (
-
- );
-};
-
-export default AllNoticePage;
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AddNoticeRadio.tsx b/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AddNoticeRadio.tsx
deleted file mode 100644
index 72208051..00000000
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AddNoticeRadio.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { InputHTMLAttributes } from 'react';
-
-import { PropsWithT, T } from '@/app/i18next';
-
-import RadioDeselected from './assets/radio-deselected.svg';
-import RadioSelected from './assets/radio-selected.svg';
-
-export interface CheckboxProps extends InputHTMLAttributes {
- label?: string;
- htmlId?: string;
- selected: string;
-}
-const AddNoticeRadio = ({
- label,
- selected,
- onChange,
- t,
- ...props
-}: PropsWithT) => {
- return (
-
-
-
-
- );
-};
-
-export default AddNoticeRadio;
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/WriteEnglishNotice.tsx b/src/app/[lng]/(common)/(needSidebar)/notice/[id]/WriteEnglishNotice.tsx
deleted file mode 100644
index ac20f0e9..00000000
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/WriteEnglishNotice.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-'use client';
-
-import dayjs, { Dayjs } from 'dayjs';
-import dynamic from 'next/dynamic';
-import { useRouter } from 'next/navigation';
-import { useRef, useState } from 'react';
-import Swal from 'sweetalert2';
-import { Editor, Editor as TinyMCEEditorRef } from 'tinymce';
-
-import LogEvents from '@/api/log/log-events';
-import sendLog from '@/api/log/send-log';
-import { attachInternationalNotice } from '@/api/notice/notice';
-import Button from '@/app/components/atoms/Button';
-import Checkbox from '@/app/components/atoms/Checkbox/Checkbox';
-import { PropsWithLng } from '@/app/i18next';
-import { useTranslation } from '@/app/i18next/client';
-import { Locale } from '@/app/i18next/settings';
-import ContentIcon from '@/assets/icons/content.svg';
-import { WarningSwal } from '@/utils/swals';
-
-interface WriteEnglishNoticeProps {
- noticeId: number;
- deadline: string | Dayjs | null;
-}
-
-const DynamicTinyMCEEditor = dynamic(
- () => import('../../../../(write)/write/TinyMCEEditor'),
- {
- ssr: false,
- },
-);
-
-const WriteEnglishNotice = ({
- noticeId,
- deadline,
- lng,
-}: WriteEnglishNoticeProps & PropsWithLng) => {
- const { t } = useTranslation(lng);
-
- const [title, setTitle] = useState('');
-
- const englishEditorRef = useRef(null);
-
- const { refresh } = useRouter();
-
- const warningSwal = WarningSwal(t);
-
- const handleSubmit = async () => {
- const englishContent = englishEditorRef.current?.getContent();
-
- if (!englishContent) {
- warningSwal(t('write.alerts.body'));
- return;
- }
-
- await attachInternationalNotice({
- contentId: 1,
- noticeId,
- title,
- body: englishContent,
- lang: 'en',
- deadline: dayjs(deadline).toDate(),
- });
-
- Swal.fire({
- text: t('write.alerts.submitSuccess'),
- icon: 'success',
- confirmButtonText: t('alertResponse.confirm'),
- }).then((result) => {
- if (result.isConfirmed) {
- refresh();
- }
- });
- };
-
- return (
-
-
-
-
- {t('zabo.writeEnglishNotice.title')}
-
-
-
-
{
- setTitle(e.target.value);
- }}
- className="content mb-4 mt-4 w-full p-0 text-2xl font-bold outline-none dark:bg-transparent"
- type="text"
- placeholder={t('zabo.writeEnglishNotice.writeTitle')}
- />
-
-
-
-
-
-
-
-
- );
-};
-
-export default WriteEnglishNotice;
diff --git a/src/app/[lng]/(group)/group/GroupItem.tsx b/src/app/[lng]/(group)/group/GroupItem.tsx
index f032dcf1..0b579dbf 100644
--- a/src/app/[lng]/(group)/group/GroupItem.tsx
+++ b/src/app/[lng]/(group)/group/GroupItem.tsx
@@ -2,7 +2,7 @@ import Image from 'next/image';
import Link from 'next/link';
import { GroupInfo } from '@/api/group/group';
-import { createTranslation, PropsWithLng } from '@/app/i18next';
+import { PropsWithLng } from '@/app/i18next';
import ArrowRight from '@/assets/icons/arrow-right.svg';
import Crown from '@/assets/icons/crown.svg';
import GroupProfileDefault from '@/assets/icons/group-profile-default.webp';
@@ -32,9 +32,7 @@ const GroupItem = async ({
{group.name}
- {group.president && (
-
- )}
+ {group.president && }
diff --git a/src/app/[lng]/(group)/group/NotInGroup.tsx b/src/app/[lng]/(group)/group/NotInGroup.tsx
index 6dc83391..7dd1ac7a 100644
--- a/src/app/[lng]/(group)/group/NotInGroup.tsx
+++ b/src/app/[lng]/(group)/group/NotInGroup.tsx
@@ -1,5 +1,4 @@
import { createTranslation, PropsWithLng } from '@/app/i18next';
-import { useTranslation } from '@/app/i18next/client';
import BonFire from '@/assets/logos/bonfire.svg';
const NotInGroup = async ({ params: { lng } }: { params: PropsWithLng }) => {
diff --git a/src/app/[lng]/(group)/group/page.tsx b/src/app/[lng]/(group)/group/page.tsx
index 0982da3f..f95d6c71 100644
--- a/src/app/[lng]/(group)/group/page.tsx
+++ b/src/app/[lng]/(group)/group/page.tsx
@@ -7,7 +7,7 @@ import { redirect } from 'next/navigation';
import { auth } from '@/api/auth/auth';
import { getGroupContainingMe, GroupInfo } from '@/api/group/group';
-import Button from '@/app/components/atoms/Button';
+import Button from '@/app/components/shared/Button';
import { createTranslation, PropsWithLng } from '@/app/i18next';
import GroupItem from './GroupItem';
diff --git a/src/app/[lng]/(group)/layout.tsx b/src/app/[lng]/(group)/layout.tsx
index c32b8ea9..5ac269b4 100644
--- a/src/app/[lng]/(group)/layout.tsx
+++ b/src/app/[lng]/(group)/layout.tsx
@@ -1,15 +1,13 @@
+import '@/app/components/layout/initDayjs';
import '@/app/globals.css';
-import '@/app/initDayjs';
import type { Viewport } from 'next';
import { ToastContainer } from 'react-toastify';
-import Footer from '@/app/components/templates/Footer';
-import Navbar from '@/app/components/templates/Navbar';
-import NavbarWrite from '@/app/components/templates/NavbarWrite';
-import { createTranslation, PropsWithLng } from '@/app/i18next';
-
-import InitClient from '../(common)/InitClient';
+import Footer from '@/app/components/layout/Footer';
+import InitClient from '@/app/components/layout/InitClient';
+import NavbarWrite from '@/app/components/layout/NavbarWrite';
+import { PropsWithLng } from '@/app/i18next';
export const viewport: Viewport = {
themeColor: '#ff4500',
@@ -22,8 +20,6 @@ export default async function Layout({
children: React.ReactNode;
params: PropsWithLng;
}) {
- const { t } = await createTranslation(lng);
-
return (
@@ -33,8 +29,7 @@ export default async function Layout({
{children}
-
-
+
diff --git a/src/app/[lng]/(common)/(needSidebar)/[category]/CategorizedNotices.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/[category]/CategorizedNotices.tsx
similarity index 67%
rename from src/app/[lng]/(common)/(needSidebar)/[category]/CategorizedNotices.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/[category]/CategorizedNotices.tsx
index d46a1150..bcc7ce2b 100644
--- a/src/app/[lng]/(common)/(needSidebar)/[category]/CategorizedNotices.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/[category]/CategorizedNotices.tsx
@@ -1,23 +1,25 @@
import React from 'react';
+import LogEvents from '@/api/log/log-events';
import { NoticeSearchParams } from '@/api/notice/notice';
import { getAllNotices } from '@/api/notice/notice-server';
-import Pagination from '@/app/components/molecules/Pagination';
-import Zabo from '@/app/components/organisms/Zabo';
+import Analytics from '@/app/components/shared/Analytics';
+import Pagination from '@/app/components/shared/Pagination';
+import Zabo from '@/app/components/shared/Zabo';
import { createTranslation, PropsWithLng } from '@/app/i18next';
-import SearchNoResult from '@/assets/search-no-result.svg';
+import SearchNoResult from '@/assets/icons/search-no-result.svg';
interface CategorizedNoticesProps {
sortByDeadline: boolean;
noticeSearchParams?: NoticeSearchParams;
page: number;
+ itemsPerPage?: number;
}
-const ITEMS_PER_PAGE = 30;
-
const CategorizedNotices = async ({
sortByDeadline,
noticeSearchParams,
+ itemsPerPage = 30,
lng,
page,
}: PropsWithLng) => {
@@ -25,8 +27,8 @@ const CategorizedNotices = async ({
const notices = await getAllNotices({
...noticeSearchParams,
- offset: page * ITEMS_PER_PAGE,
- limit: ITEMS_PER_PAGE,
+ offset: page * itemsPerPage,
+ limit: itemsPerPage,
lang: lng,
...(sortByDeadline ? { orderBy: 'deadline' } : {}),
});
@@ -38,7 +40,16 @@ const CategorizedNotices = async ({
{...notices.list.map((notice) => (
-
+
+
+
))}
@@ -46,7 +57,7 @@ const CategorizedNotices = async ({
>
) : (
diff --git a/src/app/[lng]/(common)/(needSidebar)/[category]/page.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/[category]/page.tsx
similarity index 62%
rename from src/app/[lng]/(common)/(needSidebar)/[category]/page.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/[category]/page.tsx
index 79938f6f..b06ed3b6 100644
--- a/src/app/[lng]/(common)/(needSidebar)/[category]/page.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/[category]/page.tsx
@@ -2,36 +2,39 @@ import Link from 'next/link';
import { redirect } from 'next/navigation';
import { Suspense } from 'react';
-import styles from '@/app/components/atoms/Toggle/toggle.module.css';
-import LoadingCatAnimation from '@/app/components/templates/LoadingCatAnimation';
-import { sidebarObject } from '@/app/components/templates/Sidebar/sidebarObject';
+import LogEvents from '@/api/log/log-events';
+import { sidebarObject } from '@/app/components/layout/Sidebar/sidebarObject';
+import Analytics from '@/app/components/shared/Analytics';
+import LoadingCatAnimation from '@/app/components/shared/LoadingCatAnimation';
+import styles from '@/app/components/shared/Toggle/toggle.module.css';
+import { createTranslation, PropsWithLng } from '@/app/i18next';
-import { createTranslation, PropsWithLng } from '../../../../i18next';
import CategorizedNotices from './CategorizedNotices';
import { HomePath } from './paths';
export const dynamic = 'force-dynamic';
-export default async function Home({
- params: { lng, category },
- searchParams,
-}: {
+interface CategoryPageProps {
params: PropsWithLng & {
category: HomePath;
};
searchParams?: { deadline: 'true' | 'false'; page: string };
-}) {
+}
+
+export default async function CategoryPage({
+ params: { lng, category },
+ searchParams,
+}: CategoryPageProps) {
const { t } = await createTranslation(lng);
const sortByDeadline = searchParams?.deadline === 'true' ?? false;
const page = parseInt(searchParams?.page ?? '');
-
- if (Number.isNaN(page) || page < 0) {
+ const isPageValid = !Number.isNaN(page) && page >= 0;
+ if (!isPageValid) {
redirect(
`/${lng}/${category}?page=0${sortByDeadline ? '&deadline=true' : ''}`,
);
}
-
const currentSidebarObject = sidebarObject
.flat(2)
.find(({ path }) => path === category);
@@ -47,23 +50,31 @@ export default async function Home({
- {t(title) as string}
+ {t(title)}
{category !== 'deadline' && category !== 'zigglepick' && (
-
-
-
+
+
+
+
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/Actions.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/Actions.tsx
similarity index 85%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/Actions.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/Actions.tsx
index a7f2feec..f4cacfb0 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/Actions.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/Actions.tsx
@@ -6,6 +6,8 @@ import { Trans } from 'react-i18next';
import { toast } from 'react-toastify';
import Swal from 'sweetalert2';
+import LogEvents from '@/api/log/log-events';
+import sendLog from '@/api/log/send-log';
import {
addReaction,
deleteReaction,
@@ -13,16 +15,16 @@ import {
Notice,
Reaction,
} from '@/api/notice/notice';
+import Analytics from '@/app/components/shared/Analytics';
import { useTranslation } from '@/app/i18next/client';
import { Locale } from '@/app/i18next/settings';
-import Fire from '@/assets/fire-outlined.svg';
+import AnguishedFace from '@/assets/icons/anguished-face.svg';
+import Fire from '@/assets/icons/fire-outlined.svg';
import LinkIcon from '@/assets/icons/link.svg';
+import LoudlyCryingFace from '@/assets/icons/loudly-crying-face.svg';
import ShareIcon from '@/assets/icons/share.svg';
-
-import AnguishedFace from './assets/anguished-face.svg';
-import LoudlyCryingFace from './assets/loudly-crying-face.svg';
-import SurprisedFace from './assets/surprised-face-with-open-mouth.svg';
-import ThinkingFace from './assets/thinking-face.svg';
+import SurprisedFace from '@/assets/icons/surprised-face-with-open-mouth.svg';
+import ThinkingFace from '@/assets/icons/thinking-face.svg';
const EMOJI_WIDTH = 30;
@@ -133,6 +135,7 @@ const Actions = ({ notice: { title, id, reactions }, lng }: ReactionsProps) => {
};
const handleEmojiClick = async (emoji: string, isReacted: boolean) => {
+ sendLog(LogEvents.detailClickReaction, { id, type: emoji, isReacted });
const reactions = await toggleReaction(emoji, isReacted);
if (reactions) {
@@ -164,9 +167,23 @@ const Actions = ({ notice: { title, id, reactions }, lng }: ReactionsProps) => {
/>
))}
-
-
-
+
+
+
+
+
+
+
);
};
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AddAdditionalNotice.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AddAdditionalNotice.tsx
similarity index 64%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/AddAdditionalNotice.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AddAdditionalNotice.tsx
index 6a715427..aa9e2107 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AddAdditionalNotice.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AddAdditionalNotice.tsx
@@ -5,7 +5,6 @@ import 'react-clock/dist/Clock.css';
import 'react-datetime-picker/dist/DateTimePicker.css';
import { Dayjs } from 'dayjs';
-import { RefObject, useState } from 'react';
import { PropsWithLng } from '@/app/i18next';
import { useTranslation } from '@/app/i18next/client';
@@ -14,28 +13,28 @@ import AddIcon from '@/assets/icons/add.svg';
interface AddAdditionalNoticesProps {
noticeId: number;
originallyHasDeadline: string | Dayjs | null;
- supportedLanguage: string[];
- koreanRef: RefObject
;
- englishRef: RefObject;
+ koreanContent: string;
+ englishContent?: string;
+ onKoreanContentChange: (value: string) => void;
+ onEnglishContentChange?: (value: string) => void;
}
const AddAdditionalNotice = ({
- koreanRef,
- englishRef,
- noticeId,
- supportedLanguage,
- originallyHasDeadline,
+ koreanContent,
+ englishContent,
+ onKoreanContentChange,
+ onEnglishContentChange,
lng,
}: AddAdditionalNoticesProps & PropsWithLng) => {
- const [content, setContent] = useState('');
- const [englishContent, setEnglishContent] = useState('');
-
const { t } = useTranslation(lng);
+ const isEnglishSupported =
+ englishContent !== undefined && onEnglishContentChange !== undefined;
+
return (
-
+
{t('zabo.additionalNotices.title')}
@@ -44,22 +43,21 @@ const AddAdditionalNotice = ({
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AdditionalNotices.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AdditionalNotices.tsx
similarity index 99%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/AdditionalNotices.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AdditionalNotices.tsx
index 71dea9c2..19589a01 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AdditionalNotices.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AdditionalNotices.tsx
@@ -14,6 +14,7 @@ const AdditionalNotices = async ({
lng,
}: PropsWithLng
) => {
const { t } = await createTranslation(lng);
+
return (
{additionalContents.map((content, index) => {
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AuthorActions.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AuthorActions.tsx
similarity index 56%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/AuthorActions.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AuthorActions.tsx
index 2d33cb9c..f952f97c 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/AuthorActions.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AuthorActions.tsx
@@ -2,12 +2,13 @@
import Link from 'next/link';
import { useRouter } from 'next/navigation';
-import { useState } from 'react';
import Swal from 'sweetalert2';
+import LogEvents from '@/api/log/log-events';
import { deleteNotice } from '@/api/notice/notice';
+import Analytics from '@/app/components/shared/Analytics';
+import { PropsWithLng } from '@/app/i18next';
import { useTranslation } from '@/app/i18next/client';
-import { Locale } from '@/app/i18next/settings';
import EditPencilIcon from '@/assets/icons/edit-pencil.svg';
import RemoveIcon from '@/assets/icons/remove.svg';
@@ -15,12 +16,7 @@ interface WriterActionsProps {
noticeId: number;
}
-const AuthorActions = ({
- noticeId,
- lng,
-}: WriterActionsProps & { lng: Locale }) => {
- const [isMenuOpen, setIsMenuOpen] = useState
(false);
-
+const AuthorActions = ({ noticeId, lng }: PropsWithLng) => {
const { t } = useTranslation(lng);
const router = useRouter();
@@ -56,23 +52,35 @@ const AuthorActions = ({
return (
-
-
-
{t('zabo.authorActions.edit')}
-
+
+
+
{t('zabo.authorActions.edit')}
+
+
-
+
+
);
};
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/Content.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/Content.tsx
similarity index 100%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/Content.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/Content.tsx
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/ImageStack.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/ImageStack.tsx
similarity index 67%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/ImageStack.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/ImageStack.tsx
index cd91a750..2a2be973 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/ImageStack.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/ImageStack.tsx
@@ -3,6 +3,8 @@
import Image from 'next/image';
import { useState } from 'react';
+import LogEvents from '@/api/log/log-events';
+import Analytics from '@/app/components/shared/Analytics';
import { PropsWithLng } from '@/app/i18next';
import ShowcaseModal from './ShowcaseModal';
@@ -18,7 +20,7 @@ const ImageStack = ({
sources,
alt,
lng,
-}: ImageStackProps & PropsWithLng) => {
+}: PropsWithLng) => {
const [isShowcaseOpen, setIsShowcaseOpen] = useState(false);
const [initialIndex, setInitialIndex] = useState(0);
@@ -34,14 +36,16 @@ const ImageStack = ({
{sources.map((src, i) => (
-
onImageClick(i)}
- className="shrink-0 basis-48 rounded-[10px] border-2 border-greyBorder object-cover md:basis-80"
- />
+
+ onImageClick(i)}
+ className="shrink-0 basis-48 rounded-[10px] border-2 border-greyBorder object-cover md:basis-80"
+ />
+
))}
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/NoticeInfo.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/NoticeInfo.tsx
similarity index 86%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/NoticeInfo.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/NoticeInfo.tsx
index b65b827e..2b291c36 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/NoticeInfo.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/NoticeInfo.tsx
@@ -5,10 +5,10 @@ import { Trans } from 'react-i18next/TransWithoutContext';
import { auth } from '@/api/auth/auth';
import { NoticeDetail } from '@/api/notice/notice';
-import AuthorActions from '@/app/[lng]/(common)/(needSidebar)/notice/[id]/AuthorActions';
-import Tags from '@/app/components/organisms/Tags';
+import AuthorActions from '@/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/AuthorActions';
+import Tags from '@/app/components/shared/Tags';
import { createTranslation, PropsWithLng, PropsWithT } from '@/app/i18next';
-import DefaultProfile from '@/assets/default-profile.svg';
+import DefaultProfile from '@/assets/icons/default-profile.svg';
interface NoticeInfoProps extends Omit {}
@@ -59,7 +59,9 @@ const Deadline = ({ deadline, t }: PropsWithT<{ deadline: dayjs.Dayjs }>) => {
};
const Title = ({ title }: { title: string }) => (
- {title}
+
+ {title}
+
);
const Metadata = ({
diff --git a/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/SendPushNotificationAlert.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/SendPushNotificationAlert.tsx
new file mode 100644
index 00000000..1910e45c
--- /dev/null
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/SendPushNotificationAlert.tsx
@@ -0,0 +1,152 @@
+'use client';
+
+import { useSession } from 'next-auth/react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import Swal from 'sweetalert2';
+
+import { NoticeDetail } from '@/api/notice/notice';
+import { sendNoticeAlarm } from '@/api/notice/send-alarm';
+import { PropsWithLng } from '@/app/i18next';
+import { useTranslation } from '@/app/i18next/client';
+
+import { getTimeDiff } from './getTimeDiff';
+
+interface SendPushAlarmProps
+ extends Pick {}
+
+const SendPushAlarm = ({
+ id,
+ author,
+ publishedAt,
+ lng,
+}: PropsWithLng): JSX.Element | null => {
+ const { t } = useTranslation(lng);
+
+ const [isManuallyAlarmed, setIsManuallyAlarmed] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSendPushNotification = useCallback(async () => {
+ if (isLoading) return;
+ const result = await Swal.fire({
+ text: t('write.alerts.sendPushNotice'),
+ icon: 'question',
+ showCancelButton: true,
+ confirmButtonText: t('alertResponse.confirm'),
+ cancelButtonText: t('alertResponse.cancel'),
+ });
+
+ if (!result.isConfirmed) return;
+ setIsLoading(true);
+
+ try {
+ Swal.fire({
+ text: t('write.alerts.sendingAlarmNotice'),
+ icon: 'info',
+ showConfirmButton: false,
+ allowOutsideClick: false,
+ });
+
+ const newNotice = await sendNoticeAlarm({ id }).catch(() => null);
+
+ if (!newNotice) throw new Error('No newNotice returned');
+
+ setIsManuallyAlarmed(true);
+
+ Swal.fire({
+ text: t('write.alerts.sendPushNoticeSuccess'),
+ icon: 'success',
+ confirmButtonText: t('alertResponse.confirm'),
+ });
+ } catch (error) {
+ Swal.fire({
+ text: t('write.alerts.sendPushNoticeFail'),
+ icon: 'error',
+ confirmButtonText: t('alertResponse.confirm'),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }, [t, id, isLoading]);
+
+ const { data: user } = useSession();
+ const isMyNotice = user?.user.uuid === author.uuid;
+
+ const MINUTE = 60000;
+ const TEN_SECONDS = 10000;
+ const ONE_SECOND = 1000;
+
+ const getIntervalDuration = (minutes: number, seconds: number) => {
+ if (minutes > 1) return MINUTE;
+ if (seconds > 15) return TEN_SECONDS;
+ return ONE_SECOND;
+ };
+
+ const [timeRemaining, setTimeRemaining] = useState(getTimeDiff(publishedAt));
+ useEffect(() => {
+ let isSubscribed = true;
+ if (
+ isManuallyAlarmed ||
+ timeRemaining.minutes < 0 ||
+ timeRemaining.seconds < 0
+ ) {
+ return;
+ }
+
+ const intervalDuration = getIntervalDuration(
+ timeRemaining.minutes,
+ timeRemaining.seconds
+ );
+
+ const interval = setInterval(() => {
+ if (isSubscribed) {
+ const newTimeRemaining = getTimeDiff(publishedAt);
+ if (
+ newTimeRemaining.minutes !== timeRemaining.minutes ||
+ newTimeRemaining.seconds !== timeRemaining.seconds
+ ) {
+ setTimeRemaining(newTimeRemaining);
+ }
+ }
+ }, intervalDuration);
+
+ return () => {
+ isSubscribed = false;
+ clearInterval(interval);
+ };
+ }, [timeRemaining, publishedAt, isManuallyAlarmed]);
+
+ const isEditable = timeRemaining.minutes >= 0 && timeRemaining.seconds >= 0;
+
+ const showComponent = useMemo(
+ () => isMyNotice && isEditable && !isManuallyAlarmed,
+ [isMyNotice, isEditable, isManuallyAlarmed],
+ );
+
+ return (
+
+
+ {t('zabo.sentPushNotificationAlert.title')}
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ handleSendPushNotification();
+ }
+ }}
+ >
+ {t('zabo.sentPushNotificationAlert.action')}
+
+
+
+ );
+};
+
+export default SendPushAlarm;
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/ShowcaseModal.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/ShowcaseModal.tsx
similarity index 98%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/ShowcaseModal.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/ShowcaseModal.tsx
index 5eb6d751..eebcdcd7 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/ShowcaseModal.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/ShowcaseModal.tsx
@@ -3,7 +3,7 @@
import Image from 'next/image';
import { useCallback, useEffect, useState } from 'react';
-import Button from '@/app/components/atoms/Button';
+import Button from '@/app/components/shared/Button';
import { PropsWithLng } from '@/app/i18next';
import { useTranslation } from '@/app/i18next/client';
import CloseIcon from '@/assets/icons/close.svg';
diff --git a/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/getTimeDiff.ts b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/getTimeDiff.ts
new file mode 100644
index 00000000..7002359b
--- /dev/null
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/getTimeDiff.ts
@@ -0,0 +1,14 @@
+import dayjs, { Dayjs } from 'dayjs';
+
+export const isServer = typeof window === 'undefined';
+
+const CLIENT_SERVER_TIME_OFFSET_SECONDS = 10
+export const getTimeDiff = (createdAt: Dayjs | string) => {
+ const currentTime = dayjs();
+ const diffInSeconds = dayjs(createdAt)
+ .subtract(CLIENT_SERVER_TIME_OFFSET_SECONDS, 'second')
+ .diff(currentTime, 'second');
+ const minutes = Math.floor(diffInSeconds / 60);
+ const seconds = diffInSeconds % 60;
+ return { minutes, seconds };
+};
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/loading.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/loading.tsx
similarity index 73%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/loading.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/loading.tsx
index 3936968f..48e18c23 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/loading.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/loading.tsx
@@ -1,4 +1,4 @@
-import LoadingCatAnimation from '@/app/components/templates/LoadingCatAnimation';
+import LoadingCatAnimation from '@/app/components/shared/LoadingCatAnimation';
import { PropsWithLng } from '@/app/i18next';
const Loading = ({ lng }: PropsWithLng) => (
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/not-found.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/not-found.tsx
similarity index 100%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/not-found.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/not-found.tsx
diff --git a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/page.tsx b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/page.tsx
similarity index 96%
rename from src/app/[lng]/(common)/(needSidebar)/notice/[id]/page.tsx
rename to src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/page.tsx
index 533286e9..4300f78c 100644
--- a/src/app/[lng]/(common)/(needSidebar)/notice/[id]/page.tsx
+++ b/src/app/[lng]/(with-page-layout)/(with-sidebar-layout)/notice/[id]/page.tsx
@@ -2,7 +2,6 @@ import { Metadata, ResolvingMetadata } from 'next';
import { notFound } from 'next/navigation';
import { getNotice } from '@/api/notice/get-notice';
-import { createTranslation } from '@/app/i18next';
import { Locale } from '@/app/i18next/settings';
import Actions from './Actions';
@@ -10,6 +9,7 @@ import AdditionalNotices from './AdditionalNotices';
import Content from './Content';
import ImageStack from './ImageStack';
import NoticeInfo from './NoticeInfo';
+import SendPushAlarm from './SendPushNotificationAlert';
export const generateMetadata = async (
{
@@ -72,6 +72,8 @@ const DetailedNoticePage = async ({
+
+
{
+ const { t } = useTranslation(lng);
+
+ const [mounted, setMounted] = useState(false);
+ const { theme, setTheme } = useTheme();
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ return (
+
+
+
+ {t('mypage.darkModeSettings')}
+
+ {!mounted ? (
+
+ {t('mypage.loadingDarkModeSettings')}
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default ChangeDarkModeBox;
diff --git a/src/app/[lng]/(common)/(common)/mypage/ChangeLanguageBox.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/ChangeLanguageBox.tsx
similarity index 72%
rename from src/app/[lng]/(common)/(common)/mypage/ChangeLanguageBox.tsx
rename to src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/ChangeLanguageBox.tsx
index 19a447a5..9802aa02 100644
--- a/src/app/[lng]/(common)/(common)/mypage/ChangeLanguageBox.tsx
+++ b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/ChangeLanguageBox.tsx
@@ -2,7 +2,9 @@
import { usePathname, useRouter } from 'next/navigation';
-import Toggle from '@/app/components/atoms/Toggle/Toggle';
+import LogEvents from '@/api/log/log-events';
+import Analytics from '@/app/components/shared/Analytics';
+import Toggle from '@/app/components/shared/Toggle/Toggle';
import { PropsWithLng } from '@/app/i18next';
import { useTranslation } from '@/app/i18next/client';
@@ -29,7 +31,9 @@ const ChangeLanguageBox = ({ lng }: PropsWithLng) => {
{t('mypage.switchLanguage')}
-
+
+
+
);
diff --git a/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/ClientActions.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/ClientActions.tsx
new file mode 100644
index 00000000..8816e195
--- /dev/null
+++ b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/ClientActions.tsx
@@ -0,0 +1,47 @@
+'use client';
+
+import { signOut } from 'next-auth/react';
+
+import LogEvents from '@/api/log/log-events';
+import Analytics from '@/app/components/shared/Analytics';
+import Button from '@/app/components/shared/Button';
+import { PropsWithLng } from '@/app/i18next';
+import { useTranslation } from '@/app/i18next/client';
+
+import MypageBox from './MypageBox';
+
+export default function ClientActions({ lng }: PropsWithLng) {
+ const { t } = useTranslation(lng);
+
+ const handleSignOut = () => {
+ signOut({ callbackUrl: '/' });
+ };
+
+ const handleWithdrawal = () => {
+ window.open(process.env.NEXT_PUBLIC_IDP_BASE_URL);
+ };
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/src/app/[lng]/(common)/(common)/mypage/MypageActions.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageActions.tsx
similarity index 53%
rename from src/app/[lng]/(common)/(common)/mypage/MypageActions.tsx
rename to src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageActions.tsx
index fc7bdb63..811f1556 100644
--- a/src/app/[lng]/(common)/(common)/mypage/MypageActions.tsx
+++ b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageActions.tsx
@@ -1,19 +1,14 @@
-import { cookies } from 'next/headers';
-
import { PropsWithLng } from '@/app/i18next';
-import ChangeDarkModeBox, { ColorThemeCookie } from './ChangeDarkModeBox';
+import ChangeDarkModeBox from './ChangeDarkModeBox';
import ChangeLanguageBox from './ChangeLanguageBox';
import ClientActions from './ClientActions';
export default function MypageActions({ lng }: PropsWithLng) {
- const cookieStore = cookies();
- const theme = cookieStore.get('theme') as ColorThemeCookie | undefined;
-
return (
-
+
);
diff --git a/src/app/[lng]/(common)/(common)/mypage/MypageBox.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageBox.tsx
similarity index 100%
rename from src/app/[lng]/(common)/(common)/mypage/MypageBox.tsx
rename to src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageBox.tsx
diff --git a/src/app/[lng]/(common)/(common)/mypage/MypageButton.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageButton.tsx
similarity index 52%
rename from src/app/[lng]/(common)/(common)/mypage/MypageButton.tsx
rename to src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageButton.tsx
index bd30714e..4734ce4a 100644
--- a/src/app/[lng]/(common)/(common)/mypage/MypageButton.tsx
+++ b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageButton.tsx
@@ -1,15 +1,17 @@
-'use client';
-
import Link from 'next/link';
import React from 'react';
-import { PropsWithLng } from '@/app/i18next';
+import LogEvents from '@/api/log/log-events';
+import Analytics from '@/app/components/shared/Analytics';
+import CSLink from '@/app/components/shared/CSLink/CSLink';
+import { createTranslation, PropsWithLng } from '@/app/i18next';
import { useTranslation } from '@/app/i18next/client';
import BellIcon from '@/assets/icons/bell.svg';
import PencilIcon from '@/assets/icons/edit-pencil.svg';
import FlagIcon from '@/assets/icons/white-flag.svg';
import MypageBox from './MypageBox';
+
interface MypageButtonType {
icon: React.ReactNode;
buttonText: string;
@@ -40,42 +42,46 @@ const MypageButton = ({ icon, buttonText, align }: MypageButtonType) => {
);
};
-const MypageButtons = ({ lng }: PropsWithLng) => {
- const { t } = useTranslation(lng);
+const MypageButtons = async ({ lng }: PropsWithLng) => {
+ const { t } = await createTranslation(lng);
const ICON_CLASSNAME = 'w-10 stroke-text dark:stroke-dark_white';
- const CS_PAGE_URL = 'https://cs.gistory.me/?service=Ziggle';
-
return (
-
-
}
- buttonText={t('mypage.myNotice')}
- />
-
-
-
}
- buttonText={t('mypage.remindNotice')}
- />
-
+
+
+ }
+ buttonText={t('mypage.myNotice')}
+ />
+
+
+
+
+ }
+ buttonText={t('mypage.remindNotice')}
+ />
+
+
-
-
}
- buttonText={t('mypage.feedback')}
- />
-
+
+
+ }
+ buttonText={t('mypage.feedback')}
+ />
+
+
);
diff --git a/src/app/[lng]/(common)/(common)/mypage/MypageProfile.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageProfile.tsx
similarity index 84%
rename from src/app/[lng]/(common)/(common)/mypage/MypageProfile.tsx
rename to src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageProfile.tsx
index a0e22c0e..d7a34a2d 100644
--- a/src/app/[lng]/(common)/(common)/mypage/MypageProfile.tsx
+++ b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/MypageProfile.tsx
@@ -46,13 +46,16 @@ export default function MypageProfile({
{t('mypage.info')}
- {MYPAGE_FIELDS.map((field, idx) => (
-
+ {MYPAGE_FIELDS.map(({ field, i18nKey }) => (
+
- {t(field.i18nKey)}
+ {t(i18nKey)}
- {field.field}
+ {field}
))}
diff --git a/src/app/[lng]/(common)/(common)/mypage/page.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/page.tsx
similarity index 91%
rename from src/app/[lng]/(common)/(common)/mypage/page.tsx
rename to src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/page.tsx
index d26621a3..44e50869 100644
--- a/src/app/[lng]/(common)/(common)/mypage/page.tsx
+++ b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/mypage/page.tsx
@@ -17,7 +17,7 @@ export default async function MyPage({
if (!session) redirect(`/${lng}/login`);
return (
-
+
(
diff --git a/src/app/[lng]/(common)/(common)/search/page.tsx b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/search/page.tsx
similarity index 90%
rename from src/app/[lng]/(common)/(common)/search/page.tsx
rename to src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/search/page.tsx
index 46019789..ae748dd0 100644
--- a/src/app/[lng]/(common)/(common)/search/page.tsx
+++ b/src/app/[lng]/(with-page-layout)/(without-sidebar-layout)/search/page.tsx
@@ -1,7 +1,7 @@
-import SearchAnimation from '@/app/components/templates/SearchAnimation';
import { createTranslation, PropsWithLng } from '@/app/i18next';
-import SearchResults from '../../../../components/templates/SearchResults';
+import SearchAnimation from './SearchAnimation';
+import SearchResults from './SearchResults';
const ITEMS_PER_CALL = 10;
diff --git a/src/app/[lng]/(common)/layout.tsx b/src/app/[lng]/(with-page-layout)/layout.tsx
similarity index 53%
rename from src/app/[lng]/(common)/layout.tsx
rename to src/app/[lng]/(with-page-layout)/layout.tsx
index 4d49d8a6..1d006aca 100644
--- a/src/app/[lng]/(common)/layout.tsx
+++ b/src/app/[lng]/(with-page-layout)/layout.tsx
@@ -1,14 +1,13 @@
+import '@/app/components/layout/initDayjs';
import '@/app/globals.css';
-import '@/app/initDayjs';
import type { Viewport } from 'next';
import { ToastContainer } from 'react-toastify';
-import Footer from '@/app/components/templates/Footer';
-import Navbar from '@/app/components/templates/Navbar';
-import { createTranslation, PropsWithLng } from '@/app/i18next';
-
-import InitClient from './InitClient';
+import Footer from '@/app/components/layout/Footer';
+import InitClient from '@/app/components/layout/InitClient';
+import Navbar from '@/app/components/layout/Navbar';
+import { PropsWithLng } from '@/app/i18next';
export const viewport: Viewport = {
themeColor: '#ff4500',
@@ -21,17 +20,12 @@ export default async function Layout({
children: React.ReactNode;
params: PropsWithLng;
}) {
- const { t } = await createTranslation(lng);
-
return (
-
-
{children}
-
-
-
+
{children}
+
diff --git a/src/app/[lng]/(empty)/app/page.tsx b/src/app/[lng]/(without-page-layout)/app/page.tsx
similarity index 94%
rename from src/app/[lng]/(empty)/app/page.tsx
rename to src/app/[lng]/(without-page-layout)/app/page.tsx
index 14a89d47..e20919ab 100644
--- a/src/app/[lng]/(empty)/app/page.tsx
+++ b/src/app/[lng]/(without-page-layout)/app/page.tsx
@@ -6,7 +6,7 @@ import { redirect } from 'next/navigation';
import {
appStoreLink,
playStoreLink,
-} from '@/app/components/templates/Footer/Footer';
+} from '@/app/components/layout/Footer/Footer';
const AppOpenPage = ({
searchParams,
diff --git a/src/app/[lng]/(empty)/layout.tsx b/src/app/[lng]/(without-page-layout)/layout.tsx
similarity index 81%
rename from src/app/[lng]/(empty)/layout.tsx
rename to src/app/[lng]/(without-page-layout)/layout.tsx
index 025b34b1..f156035a 100644
--- a/src/app/[lng]/(empty)/layout.tsx
+++ b/src/app/[lng]/(without-page-layout)/layout.tsx
@@ -1,7 +1,6 @@
+import InitClient from '@/app/components/layout/InitClient';
import { PropsWithLng } from '@/app/i18next';
-import InitClient from '../(common)/InitClient';
-
export default async function Layout({
children,
params: { lng },
diff --git a/src/app/[lng]/(empty)/login/page.tsx b/src/app/[lng]/(without-page-layout)/login/page.tsx
similarity index 100%
rename from src/app/[lng]/(empty)/login/page.tsx
rename to src/app/[lng]/(without-page-layout)/login/page.tsx
diff --git a/src/app/[lng]/AmplitudeProvider.tsx b/src/app/[lng]/AmplitudeProvider.tsx
new file mode 100644
index 00000000..980be94f
--- /dev/null
+++ b/src/app/[lng]/AmplitudeProvider.tsx
@@ -0,0 +1,16 @@
+'use client';
+
+import { init } from '@amplitude/analytics-browser';
+import { ReactNode, useEffect } from 'react';
+
+const AmplitudeProvider = ({ children }: { children: ReactNode }) => {
+ useEffect(() => {
+ if (!process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY) return;
+
+ init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY);
+ }, []);
+
+ return children;
+};
+
+export default AmplitudeProvider;
diff --git a/src/app/[lng]/layout.tsx b/src/app/[lng]/layout.tsx
index 3720b9e7..8dade78c 100644
--- a/src/app/[lng]/layout.tsx
+++ b/src/app/[lng]/layout.tsx
@@ -1,17 +1,17 @@
-import '@/app/initDayjs';
+import '@/app/components/layout/initDayjs';
import '@/app/globals.css';
+import { GoogleAnalytics } from '@next/third-parties/google';
import dayjs from 'dayjs';
import { dir } from 'i18next';
import type { Metadata } from 'next';
import localFont from 'next/font/local';
-import { cookies } from 'next/headers';
import Script from 'next/script';
import { createTranslation, PropsWithLng } from '@/app/i18next';
import { languages } from '@/app/i18next/settings';
-import { type ColorThemeCookie } from './(common)/(common)/mypage/ChangeDarkModeBox';
+import AmplitudeProvider from './AmplitudeProvider';
const pretendard = localFont({
src: '../../assets/fonts/PretendardVariable.woff2',
@@ -80,11 +80,8 @@ export default async function RootLayout({
}) {
dayjs.locale(lng);
- const cookieStore = cookies();
- const theme = cookieStore.get('theme') as ColorThemeCookie | undefined;
-
return (
-
+