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 = ({