From f78454c2a17a849d923c311606c7bb9204938d80 Mon Sep 17 00:00:00 2001 From: Mayne0213 Date: Tue, 6 Jan 2026 17:29:16 +0900 Subject: [PATCH] CHORE(merge): merge from develop - Initial setup and all features from develop branch - Includes: auth, deploy, docker, style fixes - K3S deployment configuration --- .github/workflows/build.yml | 110 +- .github/workflows/ci.yml | 11 +- .gitignore | 20 +- deploy/docker/Dockerfile.prod => Dockerfile | 3 + deploy/argocd/application.yaml | 42 - deploy/docker/Dockerfile.dev | 29 - deploy/docker/docker-compose.dev.yml | 64 - deploy/docker/docker-compose.yml | 39 - deploy/k8s/base/deployment.yaml | 86 - deploy/k8s/base/kustomization.yaml | 14 - deploy/k8s/base/service.yaml | 15 - .../k8s/overlays/prod/deployment-patch.yaml | 19 - deploy/k8s/overlays/prod/kustomization.yaml | 19 - deploy/k8s/overlays/prod/resourcequota.yaml | 12 - nextjs/app/(auth)/login/page.tsx | 23 + nextjs/app/(auth)/signup/page.tsx | 38 + .../(subpages)/(about)/directions/layout.tsx | 19 + .../(subpages)/(about)/directions/page.tsx | 125 + .../app/(subpages)/(about)/greeting/page.tsx | 71 + .../app/(subpages)/(about)/leaders/page.tsx | 103 + nextjs/app/(subpages)/(about)/vision/page.tsx | 228 + .../(discipling)/system/[stage]/page.tsx | 422 + .../(subpages)/(discipling)/system/page.tsx | 161 + .../app/(subpages)/(mission)/mission/page.tsx | 44 + .../(news)/announcements/[id]/page.tsx | 239 + .../(news)/announcements/create/page.tsx | 180 + .../(news)/announcements/layout.tsx | 19 + .../(subpages)/(news)/announcements/page.tsx | 197 + .../(subpages)/(news)/gallery/[id]/page.tsx | 204 + .../app/(subpages)/(news)/gallery/layout.tsx | 19 + nextjs/app/(subpages)/(news)/gallery/page.tsx | 114 + .../(subpages)/(news)/gallery/write/page.tsx | 327 + .../(subpages)/(next-gen)/generation/page.tsx | 182 + .../(subpages)/(worship)/worship/layout.tsx | 19 + .../app/(subpages)/(worship)/worship/page.tsx | 465 + nextjs/app/(subpages)/layout.tsx | 20 + nextjs/app/api/announcements/[id]/route.ts | 132 + nextjs/app/api/announcements/route.ts | 96 + nextjs/app/api/auth/[...nextauth]/route.ts | 4 + nextjs/app/api/auth/me/route.ts | 31 + nextjs/app/api/auth/signup/route.ts | 95 + .../app/api/disciple-videos/reorder/route.ts | 79 + nextjs/app/api/disciple-videos/route.ts | 148 + nextjs/app/api/files/download-url/route.ts | 32 + nextjs/app/api/files/upload-url/route.ts | 72 + nextjs/app/api/gallery/[id]/route.ts | 158 + nextjs/app/api/gallery/route.ts | 135 + nextjs/app/api/worship/reorder/route.ts | 79 + nextjs/app/api/worship/route.ts | 151 + nextjs/app/config/index.ts | 1 + nextjs/app/config/metadata.ts | 131 + nextjs/app/favicon.ico | Bin 0 -> 211598 bytes nextjs/app/globals.css | 73 + nextjs/app/layout.tsx | 49 + nextjs/app/manifest.ts | 47 + nextjs/app/opengraph-image.jpg | Bin 0 -> 124778 bytes nextjs/app/page.tsx | 18 + nextjs/app/robots.ts | 20 + nextjs/app/sitemap.ts | 94 + nextjs/app/twitter-image.jpg | Bin 0 -> 124778 bytes nextjs/components/FileUpload.tsx | 186 + nextjs/components/ImageUpload.tsx | 286 + nextjs/components/Pagination.tsx | 87 + nextjs/components/SignedImage.tsx | 70 + nextjs/components/auth/LoginForm.tsx | 132 + nextjs/components/auth/SignUpForm.tsx | 288 + nextjs/components/landing/Contact.tsx | 72 + nextjs/components/landing/FAQ.tsx | 56 + nextjs/components/landing/Hero.tsx | 144 + nextjs/components/landing/Intro.tsx | 77 + nextjs/components/landing/Ministries.tsx | 59 + nextjs/components/landing/NewsAndGallery.tsx | 211 + nextjs/components/landing/ServiceTimes.tsx | 117 + nextjs/components/landing/Welcome.tsx | 82 + .../components/providers/SessionProvider.tsx | 11 + nextjs/components/seo/JsonLd.tsx | 178 + nextjs/components/seo/MetaTags.tsx | 77 + nextjs/components/widgets/AuthButton.tsx | 81 + nextjs/components/widgets/Footer.tsx | 52 + nextjs/components/widgets/Header.tsx | 243 + nextjs/components/widgets/SubNavbar.tsx | 76 + nextjs/const/api.ts | 54 + nextjs/const/index.ts | 4 + nextjs/const/s3.ts | 23 + nextjs/const/tabs.ts | 137 + nextjs/eslint.config.mjs | 32 + nextjs/hooks/index.ts | 3 + nextjs/hooks/useAuth.ts | 65 + nextjs/hooks/useImageModal.ts | 123 + nextjs/hooks/usePagination.ts | 97 + nextjs/lib/api/http.ts | 174 + nextjs/lib/api/index.ts | 2 + nextjs/lib/auth.ts | 73 + nextjs/lib/prisma.ts | 15 + nextjs/lib/s3.ts | 44 + nextjs/lib/services/announcement.ts | 87 + nextjs/lib/services/auth.ts | 32 + nextjs/lib/services/disciple.ts | 63 + nextjs/lib/services/file.ts | 93 + nextjs/lib/services/gallery.ts | 143 + nextjs/lib/services/index.ts | 18 + nextjs/lib/services/worship.ts | 69 + nextjs/lib/tabs.ts | 116 + nextjs/lib/utils/index.ts | 2 + nextjs/lib/utils/pagination.ts | 42 + nextjs/lib/utils/seo.ts | 184 + nextjs/lib/utils/youtube.ts | 93 + nextjs/next.config.ts | 33 + nextjs/package-lock.json | 8765 +++++++++++++++++ nextjs/package.json | 54 + nextjs/postcss.config.mjs | 5 + nextjs/prisma/schema.prisma | 128 + nextjs/proxy.ts | 41 + nextjs/public/footer/cafe.webp | Bin 0 -> 4160 bytes nextjs/public/home/hero/image.webp | Bin 0 -> 36404 bytes nextjs/public/home/hero/image2.webp | Bin 0 -> 36582 bytes nextjs/public/home/hero/video1.webm | Bin 0 -> 2562324 bytes nextjs/public/home/intro/church1.webp | Bin 0 -> 1325614 bytes nextjs/public/home/intro/church2.webp | Bin 0 -> 1237840 bytes nextjs/public/home/intro/pray.webp | Bin 0 -> 3161530 bytes nextjs/public/home/welcome/circles.webp | Bin 0 -> 121874 bytes nextjs/public/icon_black.webp | Bin 0 -> 15900 bytes nextjs/public/icon_white.webp | Bin 0 -> 13540 bytes nextjs/public/logo.webp | Bin 0 -> 16074 bytes nextjs/public/opengraph-image.jpg | Bin 0 -> 124778 bytes nextjs/public/subpages/about/aboutBG.webp | Bin 0 -> 68068 bytes .../subpages/about/greetings/person.webp | Bin 0 -> 262480 bytes nextjs/public/subpages/about/leaders/1.webp | Bin 0 -> 105520 bytes nextjs/public/subpages/about/leaders/2.webp | Bin 0 -> 103768 bytes nextjs/public/subpages/about/leaders/3.webp | Bin 0 -> 192594 bytes nextjs/public/subpages/about/leaders/4.webp | Bin 0 -> 111276 bytes nextjs/public/subpages/about/leaders/5.webp | Bin 0 -> 98782 bytes nextjs/public/subpages/about/leaders/6.webp | Bin 0 -> 101474 bytes nextjs/public/subpages/about/leaders/7.webp | Bin 0 -> 114276 bytes nextjs/public/subpages/about/leaders/8.webp | Bin 0 -> 115768 bytes nextjs/public/subpages/about/leaders/9.webp | Bin 0 -> 148584 bytes .../subpages/community/communityBG.webp | Bin 0 -> 68510 bytes nextjs/public/subpages/generation/adult.webp | Bin 0 -> 408232 bytes .../subpages/generation/elementary.webp | Bin 0 -> 520012 bytes .../subpages/generation/generationBG.webp | Bin 0 -> 117596 bytes .../subpages/generation/highschool.webp | Bin 0 -> 380346 bytes nextjs/public/subpages/generation/youth.webp | Bin 0 -> 430440 bytes .../subpages/mission/mission/missionMap.webp | Bin 0 -> 333240 bytes .../mission/mission/missionMapKorea.webp | Bin 0 -> 180210 bytes nextjs/public/subpages/mission/missionBG.webp | Bin 0 -> 390134 bytes nextjs/public/subpages/system/icon1.webp | Bin 0 -> 6088 bytes nextjs/public/subpages/system/icon2.webp | Bin 0 -> 29422 bytes nextjs/public/subpages/system/icon3.webp | Bin 0 -> 23372 bytes nextjs/public/subpages/system/icon4.webp | Bin 0 -> 22484 bytes nextjs/public/subpages/system/icon5.webp | Bin 0 -> 16758 bytes nextjs/public/subpages/system/systemBG.webp | Bin 0 -> 187156 bytes nextjs/public/subpages/worship/worshipBG.webp | Bin 0 -> 204406 bytes nextjs/public/twitter-image.jpg | Bin 0 -> 124778 bytes nextjs/tsconfig.json | 42 + nextjs/types/next-auth.d.ts | 31 + scripts/common.sh | 173 - scripts/docker-build.sh | 105 - scripts/docker-cleanup.sh | 41 - services/nextjs | 1 - 159 files changed, 18365 insertions(+), 774 deletions(-) rename deploy/docker/Dockerfile.prod => Dockerfile (92%) delete mode 100644 deploy/argocd/application.yaml delete mode 100644 deploy/docker/Dockerfile.dev delete mode 100644 deploy/docker/docker-compose.dev.yml delete mode 100644 deploy/docker/docker-compose.yml delete mode 100644 deploy/k8s/base/deployment.yaml delete mode 100644 deploy/k8s/base/kustomization.yaml delete mode 100644 deploy/k8s/base/service.yaml delete mode 100644 deploy/k8s/overlays/prod/deployment-patch.yaml delete mode 100644 deploy/k8s/overlays/prod/kustomization.yaml delete mode 100644 deploy/k8s/overlays/prod/resourcequota.yaml create mode 100644 nextjs/app/(auth)/login/page.tsx create mode 100644 nextjs/app/(auth)/signup/page.tsx create mode 100644 nextjs/app/(subpages)/(about)/directions/layout.tsx create mode 100644 nextjs/app/(subpages)/(about)/directions/page.tsx create mode 100644 nextjs/app/(subpages)/(about)/greeting/page.tsx create mode 100644 nextjs/app/(subpages)/(about)/leaders/page.tsx create mode 100644 nextjs/app/(subpages)/(about)/vision/page.tsx create mode 100644 nextjs/app/(subpages)/(discipling)/system/[stage]/page.tsx create mode 100644 nextjs/app/(subpages)/(discipling)/system/page.tsx create mode 100644 nextjs/app/(subpages)/(mission)/mission/page.tsx create mode 100644 nextjs/app/(subpages)/(news)/announcements/[id]/page.tsx create mode 100644 nextjs/app/(subpages)/(news)/announcements/create/page.tsx create mode 100644 nextjs/app/(subpages)/(news)/announcements/layout.tsx create mode 100644 nextjs/app/(subpages)/(news)/announcements/page.tsx create mode 100644 nextjs/app/(subpages)/(news)/gallery/[id]/page.tsx create mode 100644 nextjs/app/(subpages)/(news)/gallery/layout.tsx create mode 100644 nextjs/app/(subpages)/(news)/gallery/page.tsx create mode 100644 nextjs/app/(subpages)/(news)/gallery/write/page.tsx create mode 100644 nextjs/app/(subpages)/(next-gen)/generation/page.tsx create mode 100644 nextjs/app/(subpages)/(worship)/worship/layout.tsx create mode 100644 nextjs/app/(subpages)/(worship)/worship/page.tsx create mode 100644 nextjs/app/(subpages)/layout.tsx create mode 100644 nextjs/app/api/announcements/[id]/route.ts create mode 100644 nextjs/app/api/announcements/route.ts create mode 100644 nextjs/app/api/auth/[...nextauth]/route.ts create mode 100644 nextjs/app/api/auth/me/route.ts create mode 100644 nextjs/app/api/auth/signup/route.ts create mode 100644 nextjs/app/api/disciple-videos/reorder/route.ts create mode 100644 nextjs/app/api/disciple-videos/route.ts create mode 100644 nextjs/app/api/files/download-url/route.ts create mode 100644 nextjs/app/api/files/upload-url/route.ts create mode 100644 nextjs/app/api/gallery/[id]/route.ts create mode 100644 nextjs/app/api/gallery/route.ts create mode 100644 nextjs/app/api/worship/reorder/route.ts create mode 100644 nextjs/app/api/worship/route.ts create mode 100644 nextjs/app/config/index.ts create mode 100644 nextjs/app/config/metadata.ts create mode 100644 nextjs/app/favicon.ico create mode 100644 nextjs/app/globals.css create mode 100644 nextjs/app/layout.tsx create mode 100644 nextjs/app/manifest.ts create mode 100644 nextjs/app/opengraph-image.jpg create mode 100644 nextjs/app/page.tsx create mode 100644 nextjs/app/robots.ts create mode 100644 nextjs/app/sitemap.ts create mode 100644 nextjs/app/twitter-image.jpg create mode 100644 nextjs/components/FileUpload.tsx create mode 100644 nextjs/components/ImageUpload.tsx create mode 100644 nextjs/components/Pagination.tsx create mode 100644 nextjs/components/SignedImage.tsx create mode 100644 nextjs/components/auth/LoginForm.tsx create mode 100644 nextjs/components/auth/SignUpForm.tsx create mode 100644 nextjs/components/landing/Contact.tsx create mode 100644 nextjs/components/landing/FAQ.tsx create mode 100644 nextjs/components/landing/Hero.tsx create mode 100644 nextjs/components/landing/Intro.tsx create mode 100644 nextjs/components/landing/Ministries.tsx create mode 100644 nextjs/components/landing/NewsAndGallery.tsx create mode 100644 nextjs/components/landing/ServiceTimes.tsx create mode 100644 nextjs/components/landing/Welcome.tsx create mode 100644 nextjs/components/providers/SessionProvider.tsx create mode 100644 nextjs/components/seo/JsonLd.tsx create mode 100644 nextjs/components/seo/MetaTags.tsx create mode 100644 nextjs/components/widgets/AuthButton.tsx create mode 100644 nextjs/components/widgets/Footer.tsx create mode 100644 nextjs/components/widgets/Header.tsx create mode 100644 nextjs/components/widgets/SubNavbar.tsx create mode 100644 nextjs/const/api.ts create mode 100644 nextjs/const/index.ts create mode 100644 nextjs/const/s3.ts create mode 100644 nextjs/const/tabs.ts create mode 100644 nextjs/eslint.config.mjs create mode 100644 nextjs/hooks/index.ts create mode 100644 nextjs/hooks/useAuth.ts create mode 100644 nextjs/hooks/useImageModal.ts create mode 100644 nextjs/hooks/usePagination.ts create mode 100644 nextjs/lib/api/http.ts create mode 100644 nextjs/lib/api/index.ts create mode 100644 nextjs/lib/auth.ts create mode 100644 nextjs/lib/prisma.ts create mode 100644 nextjs/lib/s3.ts create mode 100644 nextjs/lib/services/announcement.ts create mode 100644 nextjs/lib/services/auth.ts create mode 100644 nextjs/lib/services/disciple.ts create mode 100644 nextjs/lib/services/file.ts create mode 100644 nextjs/lib/services/gallery.ts create mode 100644 nextjs/lib/services/index.ts create mode 100644 nextjs/lib/services/worship.ts create mode 100644 nextjs/lib/tabs.ts create mode 100644 nextjs/lib/utils/index.ts create mode 100644 nextjs/lib/utils/pagination.ts create mode 100644 nextjs/lib/utils/seo.ts create mode 100644 nextjs/lib/utils/youtube.ts create mode 100644 nextjs/next.config.ts create mode 100644 nextjs/package-lock.json create mode 100644 nextjs/package.json create mode 100644 nextjs/postcss.config.mjs create mode 100644 nextjs/prisma/schema.prisma create mode 100644 nextjs/proxy.ts create mode 100644 nextjs/public/footer/cafe.webp create mode 100644 nextjs/public/home/hero/image.webp create mode 100644 nextjs/public/home/hero/image2.webp create mode 100644 nextjs/public/home/hero/video1.webm create mode 100644 nextjs/public/home/intro/church1.webp create mode 100644 nextjs/public/home/intro/church2.webp create mode 100644 nextjs/public/home/intro/pray.webp create mode 100644 nextjs/public/home/welcome/circles.webp create mode 100644 nextjs/public/icon_black.webp create mode 100644 nextjs/public/icon_white.webp create mode 100644 nextjs/public/logo.webp create mode 100644 nextjs/public/opengraph-image.jpg create mode 100644 nextjs/public/subpages/about/aboutBG.webp create mode 100644 nextjs/public/subpages/about/greetings/person.webp create mode 100644 nextjs/public/subpages/about/leaders/1.webp create mode 100644 nextjs/public/subpages/about/leaders/2.webp create mode 100644 nextjs/public/subpages/about/leaders/3.webp create mode 100644 nextjs/public/subpages/about/leaders/4.webp create mode 100644 nextjs/public/subpages/about/leaders/5.webp create mode 100644 nextjs/public/subpages/about/leaders/6.webp create mode 100644 nextjs/public/subpages/about/leaders/7.webp create mode 100644 nextjs/public/subpages/about/leaders/8.webp create mode 100644 nextjs/public/subpages/about/leaders/9.webp create mode 100644 nextjs/public/subpages/community/communityBG.webp create mode 100644 nextjs/public/subpages/generation/adult.webp create mode 100644 nextjs/public/subpages/generation/elementary.webp create mode 100644 nextjs/public/subpages/generation/generationBG.webp create mode 100644 nextjs/public/subpages/generation/highschool.webp create mode 100644 nextjs/public/subpages/generation/youth.webp create mode 100644 nextjs/public/subpages/mission/mission/missionMap.webp create mode 100644 nextjs/public/subpages/mission/mission/missionMapKorea.webp create mode 100644 nextjs/public/subpages/mission/missionBG.webp create mode 100644 nextjs/public/subpages/system/icon1.webp create mode 100644 nextjs/public/subpages/system/icon2.webp create mode 100644 nextjs/public/subpages/system/icon3.webp create mode 100644 nextjs/public/subpages/system/icon4.webp create mode 100644 nextjs/public/subpages/system/icon5.webp create mode 100644 nextjs/public/subpages/system/systemBG.webp create mode 100644 nextjs/public/subpages/worship/worshipBG.webp create mode 100644 nextjs/public/twitter-image.jpg create mode 100644 nextjs/tsconfig.json create mode 100644 nextjs/types/next-auth.d.ts delete mode 100755 scripts/common.sh delete mode 100755 scripts/docker-build.sh delete mode 100755 scripts/docker-cleanup.sh delete mode 160000 services/nextjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e4cebb..1cd626f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build Docker Image on: push: - branches: [main] + branches: [main, develop] tags: - 'v*' workflow_dispatch: @@ -13,15 +13,11 @@ env: jobs: build-and-push: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm permissions: - contents: write + contents: read packages: write - outputs: - image-tag: ${{ steps.meta.outputs.tags }} - image-digest: ${{ steps.build.outputs.digest }} - steps: - name: Checkout code uses: actions/checkout@v4 @@ -34,7 +30,7 @@ jobs: with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + password: ${{ secrets.CR_PAT }} - name: Lowercase repository name id: lowercase @@ -48,110 +44,26 @@ jobs: images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.repo }} tags: | type=ref,event=branch - type=ref,event=pr type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha,prefix={{branch}}-sha-,format=long type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image id: build uses: docker/build-push-action@v5 with: - context: ./services/nextjs - file: ./deploy/docker/Dockerfile.prod + context: ./nextjs + file: ./Dockerfile push: true + platforms: linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - - - name: Extract SHA tag - id: extract-tag - run: | - # Extract the SHA-based tag from the tags list - TAGS="${{ steps.meta.outputs.tags }}" - echo "All tags:" - echo "$TAGS" - echo "---" - - # Get commit SHA (full 40 characters) - COMMIT_SHA="${{ github.sha }}" - - # Method 1: Extract the full SHA tag from docker/metadata-action output - # docker/metadata-action creates: main-sha- - SHA_TAG=$(echo "$TAGS" | grep -oE 'main-sha-[a-f0-9]{40}' | head -n 1) - - # Method 2: If not found, try to extract any main-sha- tag (fallback) - if [ -z "$SHA_TAG" ]; then - SHA_TAG=$(echo "$TAGS" | grep -oE 'main-sha-[a-f0-9]+' | head -n 1) - if [ -n "$SHA_TAG" ]; then - echo "⚠️ Found SHA tag (may not be full 40 chars): $SHA_TAG" - fi - fi - - # Method 3: Fallback to commit SHA directly (construct the tag) - if [ -z "$SHA_TAG" ]; then - SHA_TAG="main-sha-$COMMIT_SHA" - echo "⚠️ Could not extract from tags, using commit SHA: $SHA_TAG" - fi - - if [ -z "$SHA_TAG" ]; then - echo "❌ ERROR: Failed to extract SHA tag" - exit 1 - fi - - echo "sha-tag=$SHA_TAG" >> $GITHUB_OUTPUT - echo "✅ Extracted SHA tag: $SHA_TAG" - - - name: Update kustomization with new image tag - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - # Validate that SHA_TAG is not empty - SHA_TAG="${{ steps.extract-tag.outputs.sha-tag }}" - if [ -z "$SHA_TAG" ]; then - echo "❌ ERROR: SHA_TAG is empty, cannot update kustomization" - exit 1 - fi - - echo "📝 Updating kustomization.yaml with tag: $SHA_TAG" - - # Update kustomization.yaml with new image tag - # Handle both cases: newTag: (with value) and newTag: (empty) - sed -i.bak "s|newTag:.*|newTag: $SHA_TAG|" deploy/k8s/overlays/prod/kustomization.yaml - - # Verify the update was successful - if grep -q "newTag: $SHA_TAG" deploy/k8s/overlays/prod/kustomization.yaml; then - echo "✅ Successfully updated kustomization.yaml" - rm -f deploy/k8s/overlays/prod/kustomization.yaml.bak - else - echo "❌ ERROR: Failed to update kustomization.yaml" - cat deploy/k8s/overlays/prod/kustomization.yaml - exit 1 - fi - - # Commit and push if there are changes - if git diff --quiet; then - echo "No changes to commit" - else - git add deploy/k8s/overlays/prod/kustomization.yaml - git commit -m "Update image to $SHA_TAG" - git push - echo "✅ Kustomization updated with new image tag: $SHA_TAG" - fi + build-args: | + NEXT_PUBLIC_KAKAO_MAP_KEY=${{ secrets.NEXT_PUBLIC_KAKAO_MAP_KEY }} - name: Display image information run: | - echo "✅ Image built and pushed successfully!" - echo "📦 Image tags:" + echo "Image built and pushed successfully!" + echo "Image tags:" echo "${{ steps.meta.outputs.tags }}" - echo "🔖 SHA tag: ${{ steps.extract-tag.outputs.sha-tag }}" - echo "🔖 Digest: ${{ steps.build.outputs.digest }}" - echo "" - echo "🚀 Kustomization updated with new image tag" - echo " ArgoCD will automatically detect and deploy this new image" - echo " Monitor deployment at your ArgoCD dashboard" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2879a5..785ab44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,24 +19,25 @@ jobs: with: node-version: '20' cache: 'npm' - cache-dependency-path: services/nextjs/package-lock.json + cache-dependency-path: nextjs/package-lock.json - name: Install dependencies - working-directory: services/nextjs + working-directory: nextjs run: npm ci - name: Run ESLint - working-directory: services/nextjs + working-directory: nextjs run: npm run lint - name: Build Next.js application - working-directory: services/nextjs + working-directory: nextjs run: npm run build env: NEXT_TELEMETRY_DISABLED: 1 + DATABASE_URL: ${{ secrets.DATABASE_URL }} - name: Check build output - working-directory: services/nextjs + working-directory: nextjs run: | if [ ! -d ".next" ]; then echo "Build failed: .next directory not found" diff --git a/.gitignore b/.gitignore index 158c6c2..f16f0e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -node_modules -services/nextjs/node_modules +nextjs/node_modules .pnp .pnp.* .yarn/* @@ -15,12 +14,11 @@ services/nextjs/node_modules coverage # next.js -services/nextjs/.next/ -.next/ -/out/ +nextjs/.next/ +nextjs/out/ # production -/build +nextjs/build # misc .DS_Store @@ -41,13 +39,13 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts +nextjs/tsconfig.tsbuildinfo +nextjs/next-env.d.ts # prisma -/prisma/dev.db -/prisma/dev.db-journal -/prisma/*.db -/prisma/*.db-journal +nextjs/prisma/client +nextjs/prisma/*.db +nextjs/prisma/*.db-journal # IDE .vscode diff --git a/deploy/docker/Dockerfile.prod b/Dockerfile similarity index 92% rename from deploy/docker/Dockerfile.prod rename to Dockerfile index c158229..a12df08 100644 --- a/deploy/docker/Dockerfile.prod +++ b/Dockerfile @@ -23,6 +23,9 @@ ARG DATABASE_URL="mysql://build:build@localhost:3306/build" RUN npx prisma generate # Build the application +# Next.js NEXT_PUBLIC_* variables must be available at build time +ARG NEXT_PUBLIC_KAKAO_MAP_KEY +ENV NEXT_PUBLIC_KAKAO_MAP_KEY=$NEXT_PUBLIC_KAKAO_MAP_KEY ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build diff --git a/deploy/argocd/application.yaml b/deploy/argocd/application.yaml deleted file mode 100644 index 254cf39..0000000 --- a/deploy/argocd/application.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: jaejadle - namespace: argocd - annotations: - argocd-image-updater.argoproj.io/image-list: jaejadle=ghcr.io/mayne0213/jaejadle - argocd-image-updater.argoproj.io/jaejadle.update-strategy: latest - argocd-image-updater.argoproj.io/write-back-method: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: default - - source: - repoURL: https://github.com/Mayne0213/jaejadle.git - targetRevision: main - path: deploy/k8s/overlays/prod - - destination: - server: https://kubernetes.default.svc - namespace: jaejadle - - syncPolicy: - automated: - prune: true # 매니페스트에서 제거된 리소스 자동 삭제 - selfHeal: true # 클러스터에서 수동 변경 시 자동 복구 - allowEmpty: false - - syncOptions: - - CreateNamespace=true # namespace가 없으면 자동 생성 - - PrunePropagationPolicy=foreground - - PruneLast=true - - retry: - limit: 5 - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - - revisionHistoryLimit: 10 diff --git a/deploy/docker/Dockerfile.dev b/deploy/docker/Dockerfile.dev deleted file mode 100644 index 211fada..0000000 --- a/deploy/docker/Dockerfile.dev +++ /dev/null @@ -1,29 +0,0 @@ -# trunk-ignore-all(checkov/CKV_DOCKER_3) -FROM node:20-alpine AS base - -# Install dependencies for development -RUN apk add --no-cache libc6-compat curl - -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install all dependencies (including dev dependencies) -RUN npm ci - -# Copy source code -COPY . . - -# Generate Prisma Client -RUN npx prisma generate - -# Expose port -EXPOSE 3000 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3000/api/health || exit 1 - -# Default command (can be overridden in docker-compose) -CMD ["npm", "run", "dev"] diff --git a/deploy/docker/docker-compose.dev.yml b/deploy/docker/docker-compose.dev.yml deleted file mode 100644 index 4d3e903..0000000 --- a/deploy/docker/docker-compose.dev.yml +++ /dev/null @@ -1,64 +0,0 @@ -services: - # Next.js Application (Development) - Using External Database - app: - image: jaejadle-app-dev - build: - context: ../../services/nextjs - dockerfile: ../../deploy/docker/Dockerfile.dev - container_name: jaejadle-app-dev - restart: unless-stopped - labels: - kompose.namespace: jaejadle-dev - ports: - - "3004:3000" - env_file: - - ../../.env - environment: - - NODE_ENV=development - networks: - - jaejadle-network-dev - volumes: - - ../../services/nextjs:/app - - /app/node_modules - - /app/.next - - app_logs_dev:/app/logs - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - command: > - sh -lc "npx prisma generate && npx prisma db push && npm run dev" - - # Prisma Studio - Connects to External Database - prisma-studio: - image: jaejadle-app-dev - container_name: jaejadle-prisma-studio - restart: unless-stopped - labels: - kompose.namespace: jaejadle-dev - ports: - - "5557:5555" - env_file: - - ../../.env - environment: - - NODE_ENV=development - networks: - - jaejadle-network-dev - volumes: - - ../../services/nextjs:/app - - /app/node_modules - command: npx prisma studio --port 5555 --hostname 0.0.0.0 - -volumes: - # Named volumes for data persistence - app_logs_dev: - driver: local - -networks: - jaejadle-network-dev: - driver: bridge - ipam: - config: - - subnet: 172.25.0.0/16 diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml deleted file mode 100644 index 18d0c60..0000000 --- a/deploy/docker/docker-compose.yml +++ /dev/null @@ -1,39 +0,0 @@ -services: - # Next.js Application - Using External Database - app: - image: jaejadle-app - build: - context: ../../services/nextjs - dockerfile: ../../deploy/docker/Dockerfile.prod - container_name: jaejadle-app - restart: unless-stopped - labels: - kompose.namespace: jaejadle - ports: - - 3004:3000 - env_file: - - ../../.env - environment: - - NODE_ENV=production - networks: - - jaejadle-network - volumes: - - app_logs:/app/logs - healthcheck: - test: [CMD, curl, -f, http://localhost:3000/api/health] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - -volumes: - # Named volumes for data persistence - app_logs: - driver: local - -networks: - jaejadle-network: - driver: bridge - ipam: - config: - - subnet: 172.24.0.0/16 diff --git a/deploy/k8s/base/deployment.yaml b/deploy/k8s/base/deployment.yaml deleted file mode 100644 index 3e1dc1c..0000000 --- a/deploy/k8s/base/deployment.yaml +++ /dev/null @@ -1,86 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: jaejadle-app - labels: - app: jaejadle-app -spec: - replicas: 1 - selector: - matchLabels: - app: jaejadle-app - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 1 - maxSurge: 1 - template: - metadata: - labels: - app: jaejadle-app - spec: - containers: - - name: jaejadle-app - image: ghcr.io/mayne0213/jaejadle:latest - imagePullPolicy: Always - ports: - - containerPort: 3000 - protocol: TCP - env: - - name: NODE_ENV - value: production - - name: JWT_SECRET - valueFrom: - secretKeyRef: - name: jaejadle-secret - key: jwt-secret - - name: DATABASE_URL - valueFrom: - secretKeyRef: - name: jaejadle-secret - key: database-url - - name: AWS_REGION - valueFrom: - secretKeyRef: - name: jaejadle-secret - key: aws-region - - name: AWS_S3_BUCKET_NAME - valueFrom: - secretKeyRef: - name: jaejadle-secret - key: aws-s3-bucket-name - - name: AWS_S3_BUCKET_URL - valueFrom: - secretKeyRef: - name: jaejadle-secret - key: aws-s3-bucket-url - - name: AWS_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: jaejadle-secret - key: aws-access-key-id - - name: AWS_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: jaejadle-secret - key: aws-secret-access-key - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "300m" - livenessProbe: - httpGet: - path: / - port: 3000 - initialDelaySeconds: 30 - periodSeconds: 10 - readinessProbe: - httpGet: - path: / - port: 3000 - initialDelaySeconds: 5 - periodSeconds: 5 - restartPolicy: Always diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml deleted file mode 100644 index 0674fa8..0000000 --- a/deploy/k8s/base/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - deployment.yaml - - service.yaml - -commonLabels: - app.kubernetes.io/name: jaejadle - app.kubernetes.io/component: web - -images: - - name: ghcr.io/mayne0213/jaejadle - newTag: latest diff --git a/deploy/k8s/base/service.yaml b/deploy/k8s/base/service.yaml deleted file mode 100644 index c671009..0000000 --- a/deploy/k8s/base/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: jaejadle-service - labels: - app: jaejadle-app -spec: - type: ClusterIP - ports: - - name: http - port: 80 - targetPort: 3000 - protocol: TCP - selector: - app: jaejadle-app diff --git a/deploy/k8s/overlays/prod/deployment-patch.yaml b/deploy/k8s/overlays/prod/deployment-patch.yaml deleted file mode 100644 index f6f4685..0000000 --- a/deploy/k8s/overlays/prod/deployment-patch.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: jaejadle-app - labels: - environment: production -spec: - replicas: 1 - template: - spec: - containers: - - name: jaejadle-app - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "300m" diff --git a/deploy/k8s/overlays/prod/kustomization.yaml b/deploy/k8s/overlays/prod/kustomization.yaml deleted file mode 100644 index d043e5c..0000000 --- a/deploy/k8s/overlays/prod/kustomization.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: jaejadle - -resources: - - ../../base - - resourcequota.yaml - -commonLabels: - environment: production - -# 이미지 태그 설정 -images: - - name: ghcr.io/mayne0213/jaejadle - newTag: latest - -patchesStrategicMerge: - - deployment-patch.yaml diff --git a/deploy/k8s/overlays/prod/resourcequota.yaml b/deploy/k8s/overlays/prod/resourcequota.yaml deleted file mode 100644 index 4eb6953..0000000 --- a/deploy/k8s/overlays/prod/resourcequota.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: ResourceQuota -metadata: - name: jaejadle-quota - namespace: jaejadle -spec: - hard: - requests.memory: "512Mi" - requests.cpu: "300m" - limits.memory: "1Gi" - limits.cpu: "600m" - pods: "3" diff --git a/nextjs/app/(auth)/login/page.tsx b/nextjs/app/(auth)/login/page.tsx new file mode 100644 index 0000000..cc8cec5 --- /dev/null +++ b/nextjs/app/(auth)/login/page.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import LoginForm from "@/components/auth/LoginForm"; + +export default function LoginPage() { + return ( +
+
+ {/* Header Section */} +
+

+ 로그인 +

+

+ 제자들교회에 오신 것을 환영합니다 +

+
+ + +
+
+ ); +} + diff --git a/nextjs/app/(auth)/signup/page.tsx b/nextjs/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..fb9159e --- /dev/null +++ b/nextjs/app/(auth)/signup/page.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import SignUpForm from "@/components/auth/SignUpForm"; + +export default function SignUpPage() { + return ( +
+
+ {/* Header Section */} +
+

+ 회원가입 +

+

+ 제자들교회와 함께하세요 +

+
+ + + + {/* Bottom Text */} +
+

+ 회원가입을 진행하시면{" "} + + 과{" "} + + 에 동의하는 것으로 간주됩니다. +

+
+
+
+ ); +} + diff --git a/nextjs/app/(subpages)/(about)/directions/layout.tsx b/nextjs/app/(subpages)/(about)/directions/layout.tsx new file mode 100644 index 0000000..87b37c0 --- /dev/null +++ b/nextjs/app/(subpages)/(about)/directions/layout.tsx @@ -0,0 +1,19 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '오시는 길', + description: '제자들교회 오시는 길 안내입니다. 주소: 인천광역시 서구 고산후로 95번길 32 명진프라자 3층 본당 / 4층 교육관', + openGraph: { + title: '오시는 길 | 제자들교회', + description: '제자들교회 오시는 길 - 인천광역시 서구 고산후로 95번길 32', + }, +}; + +export default function DirectionsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} + diff --git a/nextjs/app/(subpages)/(about)/directions/page.tsx b/nextjs/app/(subpages)/(about)/directions/page.tsx new file mode 100644 index 0000000..bccaa98 --- /dev/null +++ b/nextjs/app/(subpages)/(about)/directions/page.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { useState } from 'react'; +import { MapPin } from 'lucide-react'; +import { useKakaoLoader, Map, MapMarker, CustomOverlayMap } from 'react-kakao-maps-sdk'; + +// 제자들교회 좌표 (인천광역시 서구 고산후로 95번길 32) +const CHURCH_LOCATION = { + lat: 37.592754772, + lng: 126.695602263, + name: '제자들교회', +}; + +export default function DirectionsPage() { + const [isOpen, setIsOpen] = useState(true); + const kakaoMapKey = process.env.NEXT_PUBLIC_KAKAO_MAP_KEY || ''; + + const [loading, error] = useKakaoLoader({ + appkey: kakaoMapKey, + }); + + return ( +
+
+ {/* 지도 영역 */} +
+ {loading ? ( +
+

지도를 불러오는 중...

+
+ ) : error ? ( +
+

지도를 불러오는 중 오류가 발생했습니다.

+
+ ) : ( + + setIsOpen(!isOpen)} + /> + {isOpen && ( + +
+
+ {CHURCH_LOCATION.name} +
+
+
+ )} +
+ )} +
+ + {/* 교회 정보 카드 */} +
+

제자들교회 - 인천

+
+ +
+ + + + 주소 : 인천광역시 서구 고산후로 95번길 32 명진프라자 3층 본당 / 4층 교육관 +
+
+
+ + {/*
+
+
+ +

버스 이용 시

+
+ +
+

제자들교회 앞 정류장 : 102, 550, 720, 마을3

+

+ 제자들교회 사거리 정류장 : 급행11, 간선21, 환승5 +

+

+ 중앙시장 환승센터 하차 후 도보 5분 — 교회까지 직진 후 우회전 +

+

광역버스 이용 시

+

+ 인천종합터미널에서 8800번 광역버스를 타고 제자들교회 앞 정류장에서 하차하세요. 하차 후 교회까지 도보 3분입니다. +

+
+
+ +
+ +
+
+ +

지하철 이용 시

+
+ +
+

인천 1호선 `제자들역` 3번 출구 도보 7분

+

+ 3번 출구로 나와 첫 번째 사거리에서 좌회전한 뒤 300m 직진하면 교회가 보입니다. +

+

+ 공항철도 `신제자역` 하차 후 2번 출구 → 마을버스 3번 환승 → 제자들교회 앞 정류장 하차 +

+

KTX 연계 이용 시

+

+ 광명역에서 공항철도 환승 후 `신제자역`까지 이동한 뒤, 2번 출구 마을버스 3번을 이용하면 약 40분 소요됩니다. +

+
+
+ +
*/} +
+
+ ); +} diff --git a/nextjs/app/(subpages)/(about)/greeting/page.tsx b/nextjs/app/(subpages)/(about)/greeting/page.tsx new file mode 100644 index 0000000..87a7f67 --- /dev/null +++ b/nextjs/app/(subpages)/(about)/greeting/page.tsx @@ -0,0 +1,71 @@ +import { Metadata } from 'next'; +import Image from 'next/image'; +import person from '@/public/subpages/about/greetings/person.webp'; + +export const metadata: Metadata = { + title: '담임목사 인사말', + description: '제자들교회 담임목사 김경한 목사님의 인사말입니다. 세상이 줄 수 없는 놀라운 위로와 사랑을 온전히 누리며 전하는 공동체입니다.', + openGraph: { + title: '담임목사 인사말 | 제자들교회', + description: '제자들교회 담임목사 김경한 목사님의 인사말입니다.', + }, +}; + +export default function GreetingPage() { + return ( +
+
+ + {/* 인사말 섹션 */} +
+ {/* 왼쪽: 인사말 (5/7) */} +
+

+ Welcome Home! +

+ +

+ {`제자들교회를 찾아주셔서 감사드립니다. + 이 땅의 유일한 구주되신 예수님은 + 상처와 교만, 실패와 낙망으로 얼룩진 + 저희들의 인생에 찾아오셔서 + 영생과 함께 참된 기쁨과 소망을 주셨습니다.`} +

+ +

+ {`광야와 같은 세상 속에서 + 때로는 숨쉬기조차 어려운 하루하루지만, + 이곳에서 함께 만날 예수님은 + 저희들의 닫힌 숨을 다시 열어주시는 + 위로자가 되어 주실 겁니다.`} +

+ +

+ {`제자들교회는 세상이 줄 수 없는 + 그 놀라운 위로와 사랑을 + 온전히 누리며 전하는 공동체 입니다. + 이 놀라운 믿음의 여정을 함께 걸어가기를 소망합니다.`} +

+ + {/* 담임목사 이름 */} +
+

담임목사 김경한

+
+
+ + {/* 오른쪽: 이미지 (2/7) */} +
+
+ 담임목사 +
+
+
+
+
+ ); +} diff --git a/nextjs/app/(subpages)/(about)/leaders/page.tsx b/nextjs/app/(subpages)/(about)/leaders/page.tsx new file mode 100644 index 0000000..9f4e3fc --- /dev/null +++ b/nextjs/app/(subpages)/(about)/leaders/page.tsx @@ -0,0 +1,103 @@ +import { Metadata } from 'next'; +import Image from 'next/image'; +import leader1 from '@/public/subpages/about/leaders/1.webp'; + +export const metadata: Metadata = { + title: '교역자 및 직분자', + description: '제자들교회의 교역자와 직분자를 소개합니다. 담임목사, 부목사, 전도사, 장로들이 함께 섬기고 있습니다.', + openGraph: { + title: '교역자 및 직분자 | 제자들교회', + description: '제자들교회의 교역자와 직분자 소개', + }, +}; +import leader2 from '@/public/subpages/about/leaders/2.webp'; +import leader3 from '@/public/subpages/about/leaders/3.webp'; +import leader4 from '@/public/subpages/about/leaders/4.webp'; +import leader5 from '@/public/subpages/about/leaders/5.webp'; +import leader6 from '@/public/subpages/about/leaders/6.webp'; +import leader7 from '@/public/subpages/about/leaders/7.webp'; +import leader8 from '@/public/subpages/about/leaders/8.webp'; +import leader9 from '@/public/subpages/about/leaders/9.webp'; + +const LEADER_CATEGORIES = [ + { + title: '교역자', + color: '#6d96c5', + members: [ + { name: '김경한', title: '담임목사', image: leader1 }, + { name: '김종범', title: '부목사', image: leader2 }, + { name: '최하영', title: '부목사', image: leader3 }, + { name: '김윤영', title: '전도사', image: leader4 }, + { name: '설희보', title: '전도사', image: leader5 }, + { name: '서영리', title: '협력전도사', image: leader6 }, + ], + }, + { + title: '장로', + color: '#94b7d6', + members: [ + { name: '김정태', title: '명예 장로', image: leader7 }, + { name: '안종웅', title: '장로', image: leader8 }, + { name: '김현종', title: '장로', image: leader9 }, + ], + }, +]; + +export default function LeadersPage() { + return ( +
+
+
+ {LEADER_CATEGORIES.map((category, categoryIndex) => ( +
+ {/* 섹션 헤더 */} +
+
+

+ {category.title} +

+
+ + {/* 멤버 그리드 */} +
+ {category.members.map((member, memberIndex) => ( +
+ {/* 프로필 이미지 */} +
+ {member.name} + {/* 그라데이션 오버레이 */} +
+
+ + {/* 정보 */} +
+

+ {member.name} +

+

+ {member.title} +

+
+
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/nextjs/app/(subpages)/(about)/vision/page.tsx b/nextjs/app/(subpages)/(about)/vision/page.tsx new file mode 100644 index 0000000..7f4a591 --- /dev/null +++ b/nextjs/app/(subpages)/(about)/vision/page.tsx @@ -0,0 +1,228 @@ +import { Metadata } from 'next'; +import React from 'react'; +import Image from 'next/image'; +import { Target, BookOpen, HandHeart, Sprout } from 'lucide-react'; +import logo from '@/public/logo.webp'; + +export const metadata: Metadata = { + title: '교회 비전', + description: '제자들교회의 사훈과 사명을 소개합니다. 말씀 안에, 서로 사랑, 많은 열매를 맺는 성경적 제자도 공동체입니다.', + openGraph: { + title: '교회 비전 | 제자들교회', + description: '제자들교회의 사훈과 사명 - 말씀 안에, 서로 사랑, 많은 열매', + }, +}; + +const SAHUN_DATA = [ + { + number: 1, + title: '말씀 안에', + englishTitle: 'In the Word', + description: '하나님의 말씀을 삶의 중심에 두고\n성경적 가치관으로 살아가는\n믿음의 공동체', + Icon: BookOpen, + }, + { + number: 2, + title: '서로 사랑', + englishTitle: 'Love One Another', + description: '그리스도의 사랑으로 서로를 섬기고\n하나됨을 이루어가는\n사랑의 공동체', + Icon: HandHeart, + }, + { + number: 3, + title: '많은 열매', + englishTitle: 'Abundant Fruit', + description: '복음의 능력으로 영혼을 구원하고\n생명의 열매를 풍성히 맺는\n선교의 공동체', + Icon: Sprout, + }, +]; + +const CHURCH_SINJO_LEFT = [ + { + number: '01', + title: '말씀으로 살아가는 제자들교회', + englishTitle: 'Living by the Word', + subtitle: '하나님의 말씀으로', + Icon: BookOpen, + color: '#a9c6e1', + }, + { + number: '03', + title: '복음전도와 선교를 위해 존재하는 제자들교회', + englishTitle: 'For Evangelism and Mission', + subtitle: '복음의 능력으로', + Icon: Target, + color: '#6d96c5', + }, +]; + +const CHURCH_SINJO_RIGHT = [ + { + number: '02', + title: '서로 사랑하는 제자들교회', + englishTitle: 'Loving One Another', + subtitle: '그리스도의 사랑으로', + Icon: HandHeart, + color: '#94b7d6', + }, + { + number: '04', + title: '복음으로 변화되는 제자들교회', + englishTitle: 'Transformed by Gospel', + subtitle: '생명의 능력으로', + Icon: Sprout, + color: '#88aad2', + }, +]; + +export default function VisionPage() { + return ( +
+
+
+ {/* 사훈 섹션 */} +
+
+ {SAHUN_DATA.map((item, index, array) => ( +
+
+
+
+ +
+
+ {item.number} +
+
+

+ {item.title} +

+

+ {item.englishTitle} +

+

+ {item.description} +

+
+
+ ))} +
+
+ + {/* 교회 사명 섹션 */} +
+
+
+

교회 사명

+
+
+ {/* 중앙 다이아몬드 */} +
+
+
FAITH
+
+
+ + {/* 항목들 */} +
+ {/* 왼쪽 항목들 */} +
+ {CHURCH_SINJO_LEFT.map((item, index) => ( +
+
+ +
+
+

+ {item.title} +

+ {item.subtitle && ( +

{item.subtitle}

+ )} +
+
+ ))} +
+ + {/* 오른쪽 항목들 */} +
+ {CHURCH_SINJO_RIGHT.map((item, index) => ( +
+
+ +
+
+

+ {item.title} +

+ {item.subtitle && ( +

{item.subtitle}

+ )} +
+
+ ))} +
+
+
+
+ + {/* 교회 심볼 소개 섹션 */} +
+
+
+

교회 심볼

+
+
+
+
+ {/* 로고 이미지 */} +
+
+ 제자들교회 심볼 +
+
+ + {/* 설명 */} +
+
+

+ 나무는 + 생명의 근원이신 하나님을 상징하며, 교회가 하나님 안에서 든든히 뿌리내리고 있음을 나타냅니다. +

+
+
+

+ 하트 모양의 잎은 + 하나님의 사랑과 성도 간의 사랑을 의미하며, 사랑으로 하나되는 공동체를 표현합니다. +

+
+
+

+ 사람 모양은 + 예수님을 따르는 제자들을 나타내며, 함께 성장하고 열매 맺는 교회를 상징합니다. +

+
+
+

+ 블루 컬러는 + 하늘과 바다를 연상시키며, 넓고 깊은 하나님의 은혜와 평안을 의미합니다. +

+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/nextjs/app/(subpages)/(discipling)/system/[stage]/page.tsx b/nextjs/app/(subpages)/(discipling)/system/[stage]/page.tsx new file mode 100644 index 0000000..b110416 --- /dev/null +++ b/nextjs/app/(subpages)/(discipling)/system/[stage]/page.tsx @@ -0,0 +1,422 @@ +'use client'; + +import { useState, useEffect, useCallback, useRef } from 'react'; +import Image from 'next/image'; +import { useParams } from 'next/navigation'; +import { swapDiscipleVideos, type DiscipleVideo } from '@/lib/services'; +import { useAuth } from '@/hooks'; +import { extractYouTubeId, getYouTubeThumbnailUrl } from '@/lib/utils/youtube'; +import { ArrowUp, ArrowDown } from 'lucide-react'; + +// Stage별 제목 매핑 +const STAGE_TITLES: { [key: string]: string } = { + 'new-family': '새가족반', + 'basic': '기초양육반', + 'disciple': '제자훈련반', + 'evangelism': '전도훈련반', + 'ministry': '사역훈련반', +}; + +// 제자훈련반 Step 목록 +const DISCIPLE_STEPS = [ + '1단계 - 십자가', + '2단계 - 영적전투', + '3단계 - 하나님 나라', +]; + +export default function SystemStagePage() { + const params = useParams(); + const stage = params.stage as string; + const playerRef = useRef(null); + + const stageTitle = STAGE_TITLES[stage] || '제자화 시스템'; + const isDisciple = stage === 'disciple'; + + const [videos, setVideos] = useState([]); + const [selectedVideo, setSelectedVideo] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [newVideoUrl, setNewVideoUrl] = useState(''); + const [addingStep, setAddingStep] = useState(null); + + const { user } = useAuth(); + + const loadVideos = useCallback(async () => { + try { + const response = await fetch(`/api/disciple-videos?stage=${stage}`); + if (!response.ok) throw new Error('Failed to fetch videos'); + + const result = await response.json(); + const dbVideos: DiscipleVideo[] = result.data || []; + setVideos(dbVideos); + + // 첫 번째 비디오 선택 + if (dbVideos.length > 0) { + setSelectedVideo(dbVideos[0].videoUrl); + } + + setIsLoading(false); + } catch (error) { + console.error('Error loading videos:', error); + setIsLoading(false); + } + }, [stage]); + + useEffect(() => { + loadVideos(); + }, [loadVideos]); + + const handleDelete = async (video: DiscipleVideo, e: React.MouseEvent) => { + e.stopPropagation(); + + if (!user) { + alert('로그인이 필요합니다.'); + return; + } + + if (!confirm('정말 삭제하시겠습니까?')) return; + + try { + const response = await fetch(`/api/disciple-videos?id=${video.id}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete video'); + + // 로컬 state 업데이트 + setVideos(prev => prev.filter(v => v.id !== video.id)); + } catch (error) { + console.error('Error deleting video:', error); + alert('영상 삭제에 실패했습니다.'); + } + }; + + const handleAddVideo = (step: string | null = null) => { + if (!user) { + alert('로그인이 필요합니다.'); + return; + } + + setNewVideoUrl(''); + setAddingStep(step); + setIsAddModalOpen(true); + }; + + const handleSaveNewVideo = async () => { + if (!newVideoUrl) { + alert('YouTube URL을 입력해주세요.'); + return; + } + + try { + const response = await fetch('/api/disciple-videos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + stage, + step: addingStep, + videoUrl: newVideoUrl + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || error.message || 'Failed to add video'); + } + + const result = await response.json(); + const newVideo = result.data; + + // 로컬 state 업데이트 + setVideos(prev => [...prev, newVideo]); + + setIsAddModalOpen(false); + setNewVideoUrl(''); + setAddingStep(null); + } catch (error) { + console.error('Error adding video:', error); + alert(error instanceof Error ? error.message : '영상 추가에 실패했습니다.'); + } + }; + + const moveVideo = async (video: DiscipleVideo, direction: 'up' | 'down') => { + if (!user) { + alert('로그인이 필요합니다.'); + return; + } + + // 같은 step의 비디오들만 필터링 + const stepVideos = videos.filter(v => v.step === video.step); + const videoIndex = stepVideos.findIndex(v => v.id === video.id); + if (videoIndex === -1) return; + + // 이동할 새 인덱스 계산 + const newIndex = direction === 'up' ? videoIndex - 1 : videoIndex + 1; + + // 범위 체크 + if (newIndex < 0 || newIndex >= stepVideos.length) return; + + // 교환할 두 비디오의 ID + const video1Id = stepVideos[videoIndex].id; + const video2Id = stepVideos[newIndex].id; + + try { + // 서버에 순서 변경 요청 + const updatedStepVideos = await swapDiscipleVideos(video1Id, video2Id); + + // 전체 videos에서 해당 step의 비디오들만 교체 + setVideos(prev => { + const otherVideos = prev.filter(v => v.step !== video.step); + return [...otherVideos, ...updatedStepVideos]; + }); + } catch (error) { + console.error('Error swapping videos:', error); + alert('영상 순서 변경에 실패했습니다.'); + } + }; + + // step별로 비디오 그룹화 + const getVideosByStep = (step: string | null) => { + return videos + .filter(v => v.step === step) + .sort((a, b) => b.order - a.order); + }; + + if (isLoading) { + return ( +
+
로딩 중...
+
+ ); + } + + // selectedVideo의 videoUrl에서 embed용 ID 추출 + const embedVideoId = extractYouTubeId(selectedVideo); + + const renderVideoGrid = (videoList: DiscipleVideo[], step: string | null) => ( +
+ {videoList.map((video) => { + const stepVideos = videos.filter(v => v.step === step).sort((a, b) => b.order - a.order); + const index = stepVideos.findIndex(v => v.id === video.id); + + return ( +
+
{ + setSelectedVideo(video.videoUrl); + setTimeout(() => { + if (playerRef.current) { + const elementTop = playerRef.current.getBoundingClientRect().top + window.pageYOffset; + const offset = 80; + window.scrollTo({ top: elementTop - offset, behavior: 'smooth' }); + } + }, 100); + }} + className="relative aspect-video bg-linear-to-br from-gray-800 to-gray-900 overflow-hidden cursor-pointer" + > + {stageTitle} +
+
+
+
+
+
+
+ + {user && ( +
+
+ + + +
+
+ )} +
+ ); + })} +
+ ); + + return ( +
+
+ {/* Main YouTube Player */} +
+
+ {embedVideoId ? ( +