Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
84d0609
refactor::푸터 맨밑에 있지않는것과 버튼 링크 해결
kaneaigonan Apr 21, 2026
928231c
refactor::화면 축소시 홈배경도형 왼쪽고정 해결
kaneaigonan Apr 21, 2026
04055e7
Merge pull request #2 from tmdahr/lee
tmdahr Apr 21, 2026
27e144d
refactor::자동적으로 가장작은 영역 가르킴
kaneaigonan Apr 21, 2026
9bb6995
refactor 글들 시간순 정렬 해결
kaneaigonan Apr 21, 2026
fd6f156
feat :: 개인정보처리방침 및 이용약관 페이지 추가
Satellite4245 Apr 21, 2026
a991525
feat :: 페이지네이션 추가
Satellite4245 Apr 21, 2026
76a36c1
Merge pull request #5 from tmdahr/pagination
tmdahr Apr 21, 2026
0c3f8bc
Merge pull request #4 from tmdahr/terms-privacy
tmdahr Apr 21, 2026
abf2b41
Merge pull request #3 from tmdahr/lee
tmdahr Apr 21, 2026
0952c48
feat :: QnA 게시물 삭제기능 추가
Satellite4245 Apr 21, 2026
c1ea228
feat :: 공지/활동 삭제기능 추가
Satellite4245 Apr 21, 2026
585fd69
fix :: import toast 구문 추가
Satellite4245 Apr 21, 2026
619bd89
feat::조회수 증가 기능 추가
kaneaigonan Apr 21, 2026
3195336
Merge pull request #6 from tmdahr/views
tmdahr Apr 21, 2026
492ae4f
fix :: 조직도 선 끊김 해결
Satellite4245 Apr 21, 2026
b0f6ac3
Merge branch 'main' into delete
tmdahr Apr 21, 2026
2da36c7
Merge pull request #7 from tmdahr/delete
tmdahr Apr 21, 2026
bda31bd
Merge origin/main into trees
Satellite4245 Apr 22, 2026
e39977c
fix :: 조직도 배치 변경
Satellite4245 Apr 22, 2026
b398bb0
feat:글 삭제 기능 추가
tmdahr Apr 22, 2026
90ad522
Merge pull request #9 from tmdahr/delete
tmdahr Apr 22, 2026
0853973
Merge pull request #8 from tmdahr/trees
tmdahr Apr 22, 2026
83b53e6
fix :: 게시물 순서 정렬
Satellite4245 Apr 22, 2026
3d71d6a
Merge pull request #10 from tmdahr/order-organize
tmdahr Apr 22, 2026
1e64697
refactor::푸터 로고 잘안보임고침,홈페이지연결
kaneaigonan Apr 22, 2026
c312c52
Merge pull request #11 from tmdahr/logo
tmdahr Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ service cloud.firestore {
allow write: if request.auth != null && request.auth.uid == userId;
}

// QNA 컬렉션: 모두 읽기 가능, 로그인한 사용자만 작성/수정/삭제 가능
// QNA 컬렉션: 모두 읽기 가능, 로그인한 사용자 작성 가능,
// 삭제/수정은 작성자 본인이거나 관리자만 가능
match /QNA/{document} {
allow read: if true;
allow create, update, delete: if request.auth != null;
allow create: if request.auth != null;
allow update, delete: if request.auth != null &&
(request.auth.uid == resource.data.uid ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'ADMIN');
}

// QNA 댓글: 모두 읽기 가능, 로그인한 사용자만 작성 가능
Expand Down
95 changes: 79 additions & 16 deletions src/app/(main)/[datas]/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
'use client';

import { useParams } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import Loading from '@/components/ui/loading';
import { useToastStore } from '@/store/toast';
import makeDocument from "@/util/makeDocument";
import { DeleteQA } from '@/app/api/q&a';
import { DeleteAnnouncement } from '@/app/api/announcement';
import { DeleteActivity } from '@/app/api/activity';

type IndexType = {
prevId: string | null;
Expand All @@ -20,6 +24,7 @@ type CommentType = {
export default function DetailPage() {
const [commentText, setCommentText] = useState('');
const [role, setRole] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [item, setItem] = useState<any>(null);
Expand All @@ -28,6 +33,7 @@ export default function DetailPage() {
const [comments, setComments] = useState<CommentType | null>(null);

const params = useParams();
const router = useRouter();
const { datas, id } = params as { datas: string; id: string };

const [name, setName] = useState('');
Expand All @@ -48,12 +54,14 @@ export default function DetailPage() {

onAuthStateChanged(auth, async (user) => {
if (user) {
setUserId(user.uid);
const userDoc = await getDoc(doc(db, "users", user.uid));
if (userDoc.exists()) {
const userData = userDoc.data();
setRole(userData.role || 'USER'); // Assuming role is stored in user doc
}
} else {
setUserId(null);
setRole(null);
}
});
Expand Down Expand Up @@ -92,25 +100,34 @@ export default function DetailPage() {
const data = docSnap.data();
setItem({ id: docSnap.id, ...data });

// Fetch Comments (Answer)
if (data.answer) {
// Assuming answer field exists in the doc directly or in 'comment' field
// Based on legacy logic, it updates 'comment' field.
}
if (data.comment) {
setComments({ comment: data.comment });
}

// 조회수 증가 (매번 1씩 증가)
try {
const currentViews = data.views || 0;
const { updateDoc } = await import("firebase/firestore");
await updateDoc(docRef, { views: currentViews + 1 });
} catch (err) {
console.log('조회수 증가 실패', err);
}

// Fetch Prev/Next
if (data.createdAt) {
const { getFirebaseDb } = await import("@/lib/firebase");
const db = await getFirebaseDb();
if (!db) return;
const collectionName = getCollectionName(datas);
const colRef = collection(db, collectionName);
// Prev (Newer)
const prevQuery = query(colRef, orderBy("createdAt", "asc"), startAfter(data.createdAt), limit(1));

// 이전글 (Older): 현재 글보다 이전에 작성된 글 중 가장 최근 것
const prevQuery = query(colRef, orderBy("createdAt", "desc"), startAfter(docSnap), limit(1));
const prevSnap = await getDocs(prevQuery);
const prevdoc = prevSnap.empty ? null : prevSnap.docs[0];

// Next (Older)
const nextQuery = query(colRef, orderBy("createdAt", "desc"), startAfter(data.createdAt), limit(1));
// 다음글 (Newer): 현재 글보다 나중에 작성된 글 중 가장 오래된 것
const nextQuery = query(colRef, orderBy("createdAt", "asc"), startAfter(docSnap), limit(1));
const nextSnap = await getDocs(nextQuery);
const nextdoc = nextSnap.empty ? null : nextSnap.docs[0];

Expand Down Expand Up @@ -153,11 +170,37 @@ export default function DetailPage() {
setCommentText("");
setIsWritingComment(false);
} catch (e) {
console.error(e);
alert("댓글 작성 실패");
useToastStore.getState().addToast("댓글 작성에 실패했습니다.", "error");
}
}

const handleDelete = async () => {
if (!window.confirm("정말로 삭제하시겠습니까?")) return;

let result;
let redirectPath = "/";

if (datas === "QNA" || datas === "qna") {
result = await DeleteQA(id);
redirectPath = "/qna";
} else if (datas === "announcement") {
result = await DeleteAnnouncement(id);
redirectPath = "/announcement";
} else if (datas === "act" || datas === "activity") {
result = await DeleteActivity(id);
redirectPath = "/act";
}

if (result && result.status === 200) {
useToastStore.getState().addToast("성공적으로 삭제되었습니다.", "success");
router.push(redirectPath);
} else if (result) {
useToastStore.getState().addToast(result.message || "삭제에 실패했습니다.", "error");
} else {
useToastStore.getState().addToast("이 메뉴에서는 삭제 기능을 제공하지 않습니다.", "error");
}
};

if (!item) return (
<div className="flex justify-center items-center min-h-[95vh]">
<Loading />
Expand Down Expand Up @@ -189,7 +232,17 @@ export default function DetailPage() {
)}

<div className="flex flex-1 flex-col">
<h1 className="text-2xl font-bold">{item.title}</h1>
<div className="flex justify-between items-start">
<h1 className="text-2xl font-bold">{item.title}</h1>
{role === 'ADMIN' && (
<button
onClick={handleDelete}
className="px-3 py-1 bg-red-50 text-red-500 border border-red-200 rounded hover:bg-red-100 transition-colors text-sm font-medium whitespace-nowrap"
>
삭제
</button>
)}
</div>
<p className="mt-1 text-sm text-gray-500">{formatDate(item.createdAt)}</p>

<hr className="my-4 border-[#EBEBEB]" />
Expand All @@ -201,9 +254,19 @@ export default function DetailPage() {
</div>
) : (
<div className="flex flex-col gap-6">
<div className="space-y-1">
<h1 className="text-2xl font-bold break-words">{name === 'QnA' ? 'Q. ' : ''}{item.title}</h1>
<p className="text-sm text-gray-500">{formatDate(item.createdAt)}</p>
<div className="flex justify-between items-start">
<div className="space-y-1">
<h1 className="text-2xl font-bold break-words">{name === 'QnA' ? 'Q. ' : ''}{item.title}</h1>
<p className="text-sm text-gray-500">{formatDate(item.createdAt)}</p>
</div>
{(role === 'ADMIN' || (userId === item.uid && (name === 'QnA' || datas === 'QNA'))) && (
<button
onClick={handleDelete}
className="px-3 py-1 bg-red-50 text-red-500 border border-red-200 rounded hover:bg-red-100 transition-colors text-sm font-medium whitespace-nowrap"
>
삭제
</button>
)}
</div>
<div className="rounded-xl border border-[#EAEAEA] bg-white p-4 text-base leading-relaxed">
{makeDocument(item.content)}
Expand Down
4 changes: 2 additions & 2 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<div>
<div className="flex flex-col min-h-screen">
<Header />
{children}
<main className="flex-grow">{children}</main>
<Footer />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default function Home() {
return (
<div>
<ContentContainer>
<Image src={Main} alt={"background"} className="absolute top-[-20px] z-0 left-0" />
<Image src={Main} alt={"background"} className="absolute top-[-20px] z-0 left-1/2 -translate-x-1/2" />
<HomeBanner />
</ContentContainer>
<NoticeContainer />
Expand Down
18 changes: 18 additions & 0 deletions src/app/(main)/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import ContentContainer from '@/containers/main/ContentContainer';
import { PRIVACY_POLICY } from '@/util/termsData';

export default function PrivacyPage() {
return (
<ContentContainer className="py-20 min-h-screen">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-10 text-gray-900">개인정보처리방침</h1>
<div className="bg-white rounded-lg border border-gray-200 p-8 shadow-sm">
<div className="prose prose-sm max-w-none text-gray-700 whitespace-pre-wrap leading-relaxed">
{PRIVACY_POLICY}
</div>
</div>
</div>
</ContentContainer>
);
}
Loading