diff --git a/ddip/.gitignore b/.gitignore
similarity index 96%
rename from ddip/.gitignore
rename to .gitignore
index 5ef6a52..9135c73 100644
--- a/ddip/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@
# next.js
/.next/
/out/
+/next-container/
# production
/build
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..3d44941
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Ʈ õ
+/shelf/
+/workspace.xml
+# HTTP Ŭ̾Ʈ û
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/ddip.iml b/.idea/ddip.iml
new file mode 100644
index 0000000..f9f881e
--- /dev/null
+++ b/.idea/ddip.iml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..bfeb72b
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..282b76e
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..8306744
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..90686c1
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,458 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1749200115075
+
+
+ 1749200115075
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "lastFilter": {
+ "state": "OPEN",
+ "assignee": "ArgX2"
+ }
+}
+ {
+ "selectedUrlAndAccountId": {
+ "url": "https://github.com/Siul49/DDIP.git",
+ "accountId": "72c675de-739d-426a-b925-26941b57e4d3"
+ }
+}
+ {
+ "associatedIndex": 7
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1748258372673
+
+
+ 1748258372673
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "lastFilter": {
+ "state": "OPEN",
+ "assignee": "ArgX2"
+ }
+}
+ {
+ "selectedUrlAndAccountId": {
+ "url": "https://github.com/Siul49/DDIP.git",
+ "accountId": "72c675de-739d-426a-b925-26941b57e4d3"
+ }
+}
+ {
+ "associatedIndex": 7
+}
+
+
+
+
+
+
+
+
+ {
+ "keyToString": {
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "git-widget-placeholder": "release-0.2-refactory",
+ "js.debugger.nextJs.config.created.client": "true",
+ "js.debugger.nextJs.config.created.server": "true",
+ "last_opened_file_path": "C:/Users/pkpk0/WebstormProjects/ddip/src/app/components",
+ "node.js.detected.package.eslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "ts.external.directory.path": "C:\\Users\\pkpk0\\AppData\\Local\\Programs\\WebStorm\\plugins\\javascript-plugin\\jsLanguageServicesImpl\\external",
+ "vue.rearranger.settings.migration": "true"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1748258372673
+
+
+ 1748258372673
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7d1840d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# DDIP
+
+## 현재 구현기능
+### FE
+- 로그인
+- 회원가입
+- 로그아웃
+
+
+### BE
+- 로그인
+- 회원가입
+- 로그아웃
+
diff --git a/ddip/README.md b/ddip/README.md
deleted file mode 100644
index 66bb426..0000000
--- a/ddip/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/ddip/jsconfig.json b/ddip/jsconfig.json
deleted file mode 100644
index 774c46b..0000000
--- a/ddip/jsconfig.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "compilerOptions": {
- "baseUrl": "src",
- "paths": {
- "@home/*": ["app/home/*"]
- }
- }
-}
diff --git a/ddip/public/DDIP.png b/ddip/public/DDIP.png
deleted file mode 100644
index 255b5fe..0000000
Binary files a/ddip/public/DDIP.png and /dev/null differ
diff --git a/ddip/src/app/home/category.js b/ddip/src/app/home/category.js
deleted file mode 100644
index 5a3b5e2..0000000
--- a/ddip/src/app/home/category.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import Image from 'next/image'
-import { useState } from 'react';
-
-export function Category({onSelect}) {
-
- const [activeIndex, setActiveIndex] = useState(null);
- const handleClick = (selected_index) => {
- setActiveIndex(selected_index);
- if (onSelect) onSelect(selected_index);
- };
- return (
-
- 카테고리
-
- {Array.from({ length: 6 }).map((_,index_category) => (
- handleClick(index_category+1)} className="relative w-full h-full">
-
-
- ))}
-
-
- )
-}
\ No newline at end of file
diff --git a/ddip/src/app/home/item.js b/ddip/src/app/home/item.js
deleted file mode 100644
index c6126f0..0000000
--- a/ddip/src/app/home/item.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import Image from 'next/image'
-
-export function Item({onSelect}) {
- return (
-
- )
-}
\ No newline at end of file
diff --git a/ddip/src/app/home/main.js b/ddip/src/app/home/main.js
deleted file mode 100644
index 269c3be..0000000
--- a/ddip/src/app/home/main.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Image from 'next/image'
-
-export function Main() {
-
- return (
-
+ {/* 상단 */}
+
+
+ {/* 채팅 부분 */}
+
+ {messages.map((msg) => (
+
+
+
+ {/* 이름 (상대방 메시지일 때만 표시) */}
+ {msg.sender !== "me" && (
+
+ {msg.name}
+
+ )}
+ {/* 말풍선 + 시간*/}
+
+ {/* 말풍선 */}
+
+ {msg.text}
+
+ {/* 시간 */}
+
+ {msg.time}
+
+
+
+
+ ))}
+ {/* 스크롤 제일 밑에 위치한 빈 div */}
+
+
+
+ {/* 입력 부분 */}
+
+ setInput(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleSend()}
+ className="flex-1 px-2 text-sm focus:outline-none"
+ placeholder="메시지를 입력하세요."
+ />
+
+
+
+
+ );
+}
diff --git a/src/app/components/shortcut/post/post-big.js b/src/app/components/shortcut/post/post-big.js
new file mode 100644
index 0000000..b134e21
--- /dev/null
+++ b/src/app/components/shortcut/post/post-big.js
@@ -0,0 +1,124 @@
+'use client';
+
+import { useState } from 'react';
+import { useRef } from 'react';
+import ConfirmModal from '@components/shortcut/post/post-confirm-modal';
+
+export default function WritingPage() {
+ const [showConfirm, setShowConfirm] = useState(false);
+
+ const handleSubmit = () => {
+ console.log('등록 처리 완료');
+ setShowConfirm(false);
+ onClose();
+ };
+
+ const fileInputRef = useRef(null);
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ return (
+
+ {/*
상품 등록 */}
+
+
+ {/* 등록 확인 모달 */}
+ {showConfirm && (
+
setShowConfirm(false)}
+ onConfirm={handleSubmit}
+ />
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/components/shortcut/post/post-box.js b/src/app/components/shortcut/post/post-box.js
new file mode 100644
index 0000000..d27ef67
--- /dev/null
+++ b/src/app/components/shortcut/post/post-box.js
@@ -0,0 +1,272 @@
+'use client';
+
+import {useEffect, useState} from 'react';
+import ConfirmModal from './post-confirm-modal';
+import WritingPage from './post-big';
+
+export default function PostBox({ onClose }) {
+ const [showConfirm, setShowConfirm] = useState(false);
+ const [showFullPage, setShowFullPage] = useState(false);
+ const [error, setError] = useState('');
+ const [user, setUser] = useState(null)
+ const [form, setForm] = useState({
+ title: '',
+ description: '',
+ writer: '',
+ itemCategory: '',
+ tradeType: '',
+ totalNumberOfRecruits: 1,
+ numberOfRecruitedPersonnel:0,
+ totalPrice: 1,
+ pricePerEachPerson: 1,
+ image: null,
+ });
+
+
+ useEffect(() => {
+ fetch('/api/auth/check')
+ .then(res => res.json())
+ .then(data => {
+ if (data.user) setUser(data.user);
+ })
+ .catch(error => console.error('유저 정보 불러오기 실패:', error));
+ }, []);
+
+
+ useEffect(() => {
+ if (form.totalPrice > 0 && form.totalNumberOfRecruits > 0) {
+ const calculatedPrice = form.totalPrice / form.totalNumberOfRecruits;
+ setForm(prev => ({
+ ...prev,
+ pricePerEachPerson: calculatedPrice
+ }));
+ }
+ }, [form.totalPrice, form.totalNumberOfRecruits]);
+
+ // input 값이 바뀔 때마다 form state 업데이트
+ const handleChange = (e) => {
+ const { name, value, type } = e.target;
+ setForm((prev) => ({
+ ...prev,
+ [name]: type === 'number' ? Number(value) : value,
+ }));
+ };
+
+ // 이미지 파일 따로 처리
+ const handleImageChange = (e) => {
+ setForm((prev) => ({
+ ...prev,
+ image: e.target.files[0], // 실제 파일 객체 저장
+ }));
+ };
+
+ // 등록 처리 함수 수정본
+ const handleSubmit = async () => {
+ setShowConfirm(false);
+
+ try {
+ if (!user?.nickname) throw new Error('로그인 정보가 없습니다');
+ if (!form.itemCategory || !form.tradeType) {
+ throw new Error('카테고리와 거래방식을 선택해주세요');
+ }
+
+ const payload = {
+ ...form,
+ writer: user.nickname,
+ };
+
+ let body, headers;
+
+ if (form.image) {
+ body = new FormData();
+ Object.entries(payload).forEach(([key, value]) => {
+ if (value !== null) {
+ if (key === 'image') {
+ body.append(key, value, value.name);
+ } else {
+ body.append(key, value.toString()); // 모든 값을 문자열로 변환
+ }
+ }
+ });
+ headers = undefined;
+ } else {
+ body = JSON.stringify(payload);
+ headers = { 'Content-Type': 'application/json' };
+ }
+
+ const response = await fetch('/api/post/write', { method: 'POST', headers, body });
+
+ // 응답이 JSON인지 확인
+ const contentType = response.headers.get('content-type') || '';
+ if (!contentType.includes('application/json')) {
+ const text = await response.text();
+ throw new Error(`서버 응답 오류: ${text.slice(0, 100)}`);
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ alert('띱 올리기 완료!');
+ onClose();
+ } else {
+ setError(result.message || '서버 오류 발생');
+ }
+ } catch (err) {
+ setError(err.message);
+ console.error('등록 실패:', err);
+ }
+ };
+
+
+ const handleExpand = () => {
+ setShowFullPage(true);
+ };
+
+ if (showFullPage) {
+ return
;
+ }
+
+ return (
+
+
+
상품 등록
+
+ {/* 이미지 업로드 */}
+
+ 상품이미지
+
+ {form.image && {form.image.name} }
+
+
+ {/* 제목 */}
+
+ 제목
+
+
+
+ {/* 금액 + 인원 */}
+
+
+ {/* 카테고리 + 거래방식 */}
+
+
+ 카테고리
+
+ 카테고리
+ 식재료
+ 간편식/냉동식품
+ 생활용품
+ 대용량
+ 배달음식
+ 나눔템
+
+
+
+ 거래방식
+
+ 거래 방식
+ 직거래
+ 택배
+ 기타
+
+
+
+
+ {/* 상세설명 */}
+
+ 상세설명
+
+
+
+ {/* 등록 버튼 */}
+ setShowConfirm(true)}
+ >
+ 등록하기
+
+
+
+ {/* 에러 메시지 표시 */}
+ {error && (
+
{error}
+ )}
+
+ {/* 등록 확인 모달 */}
+ {showConfirm && (
+
setShowConfirm(false)}
+ onConfirm={handleSubmit}
+ />
+ )}
+
+
+ ⇱
+
+
+
+ ✖
+
+
+
+ );
+}
diff --git a/src/app/components/shortcut/post/post-confirm-modal.js b/src/app/components/shortcut/post/post-confirm-modal.js
new file mode 100644
index 0000000..55dd875
--- /dev/null
+++ b/src/app/components/shortcut/post/post-confirm-modal.js
@@ -0,0 +1,13 @@
+export default function ConfirmModal({ onConfirm, onCancel }) {
+ return (
+
+
+
상품을 등록하시겠습니까?
+
+ 닫기
+ 등록하기
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/components/shortcut/shortcut.js b/src/app/components/shortcut/shortcut.js
new file mode 100644
index 0000000..ed8eeda
--- /dev/null
+++ b/src/app/components/shortcut/shortcut.js
@@ -0,0 +1,62 @@
+import Image from 'next/image';
+import ChatBox from './chat/chatting';
+import PostBox from './post/post-box';
+
+import {useEffect, useState} from "react";
+
+export default function Shortcut({}) {
+ const [isCOpen, setIsCOpen] = useState(false);
+ const [isPOpen, setIsPOpen] = useState(false);
+
+ const [user, setUser] = useState(null);
+ useEffect(() => {
+ fetch('/api/auth/check')
+ .then(res => res.json())
+ .then(data => setUser(data.user))
+ }, [])
+
+ const toggleChat = () => setIsCOpen(!isCOpen);
+ const togglePost = () => setIsPOpen(!isPOpen);
+
+ return (
+ <>
+ {user ? (
+ <>
+
+
+ {isCOpen &&
}
+
+
+
+
+ {isPOpen &&
}
+ >
+ ) : (
+ <>
+
alert('로그인을 해야 사용할 수 있는 기능입니다')}>
+
+
+
+
+ alert('로그인을 해야 사용할 수 있는 기능입니다')}/>
+
+ >
+ )
+ }
+ >
+
+ )
+}
\ No newline at end of file
diff --git a/ddip/src/app/favicon.ico b/src/app/favicon.ico
similarity index 100%
rename from ddip/src/app/favicon.ico
rename to src/app/favicon.ico
diff --git a/src/app/feature/auth/components/agreement.js b/src/app/feature/auth/components/agreement.js
new file mode 100644
index 0000000..ff24ce2
--- /dev/null
+++ b/src/app/feature/auth/components/agreement.js
@@ -0,0 +1,45 @@
+// [id]/AgreementBox.jsx
+export default function AgreementBox({ agreed, onChange }) {
+ return (
+
개인정보 제공 동의
+
+
+
[개인정보 수집 및 이용 동의서]
+ 회사는 개인정보 보호법 등 관련 법령에 따라 이용자의 개인정보를 보호하고,
+ 적법하게 수집·이용하고자 아래와 같이 동의를 받고자 합니다.
+
+
+
1. 수집하는 개인정보 항목
+ 필수 항목: 이름, 별명, 전화번호, 전화번호, 비밀번호
+ 선택 항목: 프로필 이미지, 이메일 주소
+
+
2. 개인정보 수집·이용 목적
+ 회원 가입 및 본인 확인
+ 서비스 제공 및 맞춤형 정보 제공
+ 고객 문의 응대 및 분석자료 정리
+
+
3. 보유 및 이용 기간
+ 수집일로부터 회원정보 수집 및 이용 목적 달성 시까지 보관
+ 단, 관련 법령에 따라 보존이 필요한 경우에는 해당 기간 동안 보존
+
+
4. 동의 거부 권리 및 불이익
+ 귀하는 개인정보 수집·이용에 동의하지 않을 수 있습니다.
+ 단, 필수 항목에 대한 동의를 거부할 경우 서비스 이용이 제한될 수 있습니다.
+
+
+
+ onChange(e.target.checked)}
+ className="mr-2"
+ />
+ 위 내용을 충분히 이해하였으며, 개인정보 수집 및 이용에 동의합니다.
+
+
+
+
+ );
+}
diff --git a/src/app/feature/auth/components/input.js b/src/app/feature/auth/components/input.js
new file mode 100644
index 0000000..0ccd48b
--- /dev/null
+++ b/src/app/feature/auth/components/input.js
@@ -0,0 +1,22 @@
+export default function Input(
+ { label, type = "text", name, value, onChange, placeholder, className="", }) {
+
+ return (
+
+
+ {label}
+
+
+
+ );
+}
diff --git a/src/app/feature/auth/components/login-form.js b/src/app/feature/auth/components/login-form.js
new file mode 100644
index 0000000..fdb5226
--- /dev/null
+++ b/src/app/feature/auth/components/login-form.js
@@ -0,0 +1,91 @@
+'use client'
+
+import { useState } from 'react';
+import { validatePassword } from './validate';
+import Input from './input';
+import Image from "next/image";
+import { useRouter } from 'next/navigation';
+
+export default function LoginForm() {
+ const router = useRouter();
+
+ const [form, setForm] = useState({
+ userid: '',
+ userpw: '',
+ });
+ const [error, setError] = useState('');
+ const handleChange = (field) => (e) => {
+ setForm((prev) => ({ ...prev, [field]: e.target.value }));
+ };
+
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError('');
+ // 비밀번호 유효성 검사
+ if (!validatePassword(form.userpw)) {
+ return setError('비밀번호는 8자 이상 입력해주세요');
+ }
+
+ try {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(form),
+ });
+ const result = await response.json();
+
+ if (result.success) {
+ alert('아주 심각한 에러입니다!');
+ alert('아주 심각한 에러입니다!');
+ alert('아주 심각한 에러입니다!');
+ alert('아주 심각한 에러입니다!');
+ alert('아주 심각한 에러입니다!');
+ alert('사실 에러 아니지롱 데헷😋');
+ alert(result.nickname + '님 환영합니다')
+ await router.push('/');
+ } else {
+ setError(result.message || '로그인에 실패했습니다.');
+ }
+ } catch (err) {
+ setError('로그인 요청 중 오류가 발생했습니다.');
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/feature/auth/components/logout-button.js b/src/app/feature/auth/components/logout-button.js
new file mode 100644
index 0000000..a117c23
--- /dev/null
+++ b/src/app/feature/auth/components/logout-button.js
@@ -0,0 +1,23 @@
+// 클라이언트 컴포넌트
+'use client'
+import { useRouter } from 'next/navigation'
+
+export default function LogoutButton() {
+ const router = useRouter();
+ const handleLogout = async () => {
+ await fetch('/api/auth/logout', { method: 'POST' });
+ setTimeout(() => {
+ window.location.reload()
+ router.push('/')
+ }, 50);
+ };
+
+ return (
+
+ 로그아웃
+
+ );
+}
diff --git a/src/app/feature/auth/components/signup-form.js b/src/app/feature/auth/components/signup-form.js
new file mode 100644
index 0000000..f905f3d
--- /dev/null
+++ b/src/app/feature/auth/components/signup-form.js
@@ -0,0 +1,141 @@
+'use client'
+
+import { useState } from 'react';
+import { validateEmail, validatePassword } from './validate';
+import Input from './input';
+import AgreementBox from './agreement';
+import Image from "next/image";
+
+export default function SignupForm({setStatus}) {
+ const [agreed, setAgreed] = useState(false);
+ const [form, setForm] = useState({
+ userid: '',
+ userpw: '',
+ checkpw: '',
+ nickname: '',
+ username: '',
+ phone: '',
+ address: '',
+ email: '',
+ });
+
+
+ const [error, setError] = useState('');
+
+ const handleChange = (field) => (e) => {
+ setForm((prev) => ({ ...prev, [field]: e.target.value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!agreed) {
+ alert("개인정보 수집 및 이용에 동의해주세요.");
+ return;
+ }
+
+ if (!validateEmail(form.email)) return setError('유효한 이메일을 입력해주세요');
+ if (!validatePassword(form.userpw)) return setError('비밀번호는 8자 이상 입력해주세요');
+
+ setError('');
+
+ if (form.userpw !== form.checkpw) {
+ return setError("비밀번호가 일치하지 않습니다.");
+ }
+
+ const response = await fetch("/api/auth/signup", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(form),
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ alert(result.message);
+ setStatus(true);
+ if (typeof setStatus === 'function') {
+ setStatus(true);
+ }
+ location.reload();
+
+ } else {
+ alert(result.message);
+ }
+ };
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/feature/auth/components/validate.js b/src/app/feature/auth/components/validate.js
new file mode 100644
index 0000000..b332ab8
--- /dev/null
+++ b/src/app/feature/auth/components/validate.js
@@ -0,0 +1,7 @@
+export function validateEmail(email) {
+ return /\S+@\S+\.\S+/.test(email);
+}
+
+export function validatePassword(pw) {
+ return pw.length >= 8;
+}
\ No newline at end of file
diff --git a/src/app/feature/auth/page.js b/src/app/feature/auth/page.js
new file mode 100644
index 0000000..23db261
--- /dev/null
+++ b/src/app/feature/auth/page.js
@@ -0,0 +1,41 @@
+'use client'
+
+import SignupForm from './components/signup-form';
+import LoginForm from "./components/login-form";
+import { useState } from "react";
+import Nav from "@components/common/nav";
+
+
+export default function SignUp() {
+ const [status, setStatus] = useState(false);
+
+ return (
+
+
+
+
+
+ {status
+ ?
+ :
+ }
+
+ {/*
+ 비밀번호 찾기, 아이디 찾기 구현
+ 이메일로 코드 보내고 인증 같은 절차 구현
+ */}
+
+ setStatus(!status)}
+ className="mb-30 text-blue-500 underline"
+ >
+ {status ? '로그인으로 돌아가기' : '회원가입'}
+
+
+
+ );
+}
+
diff --git a/src/app/feature/category/components/category-form.js b/src/app/feature/category/components/category-form.js
new file mode 100644
index 0000000..cef9128
--- /dev/null
+++ b/src/app/feature/category/components/category-form.js
@@ -0,0 +1,40 @@
+'use client';
+
+import Image from 'next/image'
+import category_lists from '@constants/simpleDB';
+import {useRouter} from "next/navigation";
+import {useState} from "react";
+
+export default function Category({onSelect}) {
+
+ const router = useRouter();
+ const [activeCategory, setActiveCategory] = useState('');
+ const handleClick = (selected_index) => {
+ setActiveCategory(selected_index);
+ if (onSelect) onSelect(selected_index);
+ router.push('/feature/category')
+ };
+
+ return (
+
+ 카테고리
+
+ {category_lists.map((item) => (
+
handleClick(item.value)}
+ className="relative w-full h-full p-4
+ flex flex-col justify-center text-center">
+
+
+
+
+ {item.name || '이름 없음'}
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/app/feature/category/components/item-list.js b/src/app/feature/category/components/item-list.js
new file mode 100644
index 0000000..f91b18e
--- /dev/null
+++ b/src/app/feature/category/components/item-list.js
@@ -0,0 +1,60 @@
+'use client';
+
+import Image from 'next/image';
+import { useState, useEffect } from 'react';
+
+
+export default function ItemList({ selectedCategoryValue, onSelect }) {
+ const [items, setItems] = useState([]);
+ const [activeIndex, setActiveIndex] = useState(null);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const res = await fetch('/api/item');
+ if (!res.ok) throw new Error('API 호출 실패');
+ const data = await res.json();
+ setItems(data);
+ } catch (error) {
+ console.error('데이터 불러오기 실패:', error);
+ }
+ };
+ fetchData();
+ }, []);
+
+
+ const handleClick = (id) => {
+ setActiveIndex(id);
+ if (onSelect) onSelect(id);
+ };
+
+ const filteredItems = selectedCategoryValue
+ ? items.filter(item => item.value === selectedCategoryValue)
+ : items;
+ console.log('items:',items);
+
+ return (
+
+
+
+ {filteredItems.map((item) => (
+
handleClick(item.itemId)}
+ >
+
+ {item.title}
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/feature/category/page.js b/src/app/feature/category/page.js
new file mode 100644
index 0000000..e0a6126
--- /dev/null
+++ b/src/app/feature/category/page.js
@@ -0,0 +1,25 @@
+'use client'
+
+import ItemList from "./components/item-list";
+import {useState} from "react";
+import Category from "./components/category-form";
+import Nav from "@components/common/nav";
+
+
+export default function CategoryPage() {
+ const [selectedCategory, setSelectedCategory] = useState('ingredient');
+ const [selectedProduct, setSelectedProduct] = useState(null);
+
+ return(
+
+
+
+
+
+
+
+
{selectedCategory}
+
+
+ )
+}
diff --git a/src/app/feature/profile/components/info-sidebar.js b/src/app/feature/profile/components/info-sidebar.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/feature/profile/components/my-info-form.js b/src/app/feature/profile/components/my-info-form.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/feature/profile/components/my-sidebar.js b/src/app/feature/profile/components/my-sidebar.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/feature/profile/components/myproduct-managing-form.js b/src/app/feature/profile/components/myproduct-managing-form.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/feature/profile/page.js b/src/app/feature/profile/page.js
new file mode 100644
index 0000000..e69de29
diff --git a/ddip/src/app/globals.css b/src/app/globals.css
similarity index 53%
rename from ddip/src/app/globals.css
rename to src/app/globals.css
index 94f37cb..922b8a7 100644
--- a/ddip/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,14 +1,20 @@
@import "tailwindcss";
-
+@font-face {
+ font-family: 'Pretendardvariable';
+ src: url('../../public/fonts/PretendardVariable.woff2') format('woff2');
+ font-style: normal;
+ font-display: swap;
+}
body {
- font-family: Arial, Helvetica, sans-serif;
+ font-family: 'Pretendardvariable', Arial, Helvetica, sans-serif;
}
+
.searchslot{
@apply text-center rounded-full bg-[#FFFCED]
- shadow-green-500 hover:shadow-xl
+ shadow-[#aaaa88] hover:shadow-lg
transition-shadow duration-250 ease-out;
}
@@ -19,3 +25,7 @@ body {
.select-transition{
@apply transition-all duration-100 ease-linear cursor-pointer ;
}
+
+html,body {
+ height: 100%;
+}
diff --git a/src/app/home/page.js b/src/app/home/page.js
new file mode 100644
index 0000000..b5999fe
--- /dev/null
+++ b/src/app/home/page.js
@@ -0,0 +1,27 @@
+import Image from "next/image";
+
+
+export default function Main() {
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ SEARCH
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/item/model/item.js b/src/app/item/model/item.js
new file mode 100644
index 0000000..9e87d02
--- /dev/null
+++ b/src/app/item/model/item.js
@@ -0,0 +1,19 @@
+import mongoose from 'mongoose';
+
+const itemSchema = new mongoose.Schema({
+ title: { type: String, required: true, unique: true },
+ description: { type: String, required: true },
+ writer: { type: String, required: true },
+ itemCategory: { type: String, required: true },
+ totalNumberOfRecruits: { type: Number, required: true },
+ numberOfRecruitedPersonnel: { type: Number, required: true },
+ totalPrice: { type: Number, required: true },
+ pricePerEachPerson: { type: Number, required: true },
+ tradeType: { type: String, required: true },
+ createdAt: { type: Date, default: Date.now }
+});
+
+
+const Item = mongoose.models.Item || mongoose.model('ddip', itemSchema,'item');
+
+export default Item;
diff --git a/src/app/item/page.js b/src/app/item/page.js
new file mode 100644
index 0000000..bc50305
--- /dev/null
+++ b/src/app/item/page.js
@@ -0,0 +1,87 @@
+import { useState, useEffect } from 'react';
+import Image from 'next/image';
+
+export default function ItemExplain({ onSelect }) {
+ const [allData, setAllData] = useState([]);
+ const [productData, setProductData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ // 전체 데이터 불러오기
+ const fetchAll = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const res = await fetch('/api/item');
+ if (!res.ok) throw new Error('데이터 불러오기 실패');
+ const data = await res.json();
+ setAllData(data);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchAll();
+ }, []);
+
+ useEffect(() => {
+ if (!onSelect || allData.length === 0) {
+ setProductData(null);
+ return;
+ }
+ // id로 필터링
+ const found = allData.find(item => item._id === onSelect);
+ setProductData(found || null);
+ }, [onSelect, allData]);
+
+ if (loading) return
로딩중...
;
+ if (error) return
{error}
;
+ if (!productData) return
선택된 제품 정보를 찾을 수 없습니다.
;
+
+ return (
+
+
+
+
+
+
+ 제품명: {productData.title || '이름 없음'}
+
+
+ 상세설명: {productData.description}
+
+
+ DDIP: {productData.totalPrice}원 / {productData.totalNumberOfRecruits}명
+
+
+ 현재인원
+
+
+ {Array.from({ length: productData.numberOfRecruitedPersonnel }).map((_, idx) => (
+
+
+
+ ))}
+ {Array.from({ length: productData.totalNumberOfRecruits - productData.numberOfRecruitedPersonnel }).map((_, idx) => (
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ddip/src/app/layout.js b/src/app/layout.js
similarity index 75%
rename from ddip/src/app/layout.js
rename to src/app/layout.js
index 7bf337d..bf34568 100644
--- a/ddip/src/app/layout.js
+++ b/src/app/layout.js
@@ -1,5 +1,6 @@
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
+import Footer from "@components/common/footer";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -20,10 +21,15 @@ export default function RootLayout({ children }) {
return (
{children}
+
+
);
}
+
diff --git a/src/app/page.js b/src/app/page.js
new file mode 100644
index 0000000..2d3a476
--- /dev/null
+++ b/src/app/page.js
@@ -0,0 +1,27 @@
+'use client'
+
+import Category from './feature/category/components/category-form';
+import Main from './home/page'
+import Footer from "@components/common/footer";
+import Nav from "@components/common/nav";
+import {useEffect, useState} from "react";
+import Shortcut from "@components/shortcut/shortcut";
+
+export default function Home() {
+ const [selectedCategory, setSelectedCategory] = useState('ingredient');
+ const [user, setUser] = useState(null);
+ useEffect(() => {
+ fetch('/api/auth/check')
+ .then(res => res.json())
+ .then(data => setUser(data.user))
+ }, [])
+
+ return (
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/user/model/User.js b/src/app/user/model/User.js
new file mode 100644
index 0000000..bd0b0f6
--- /dev/null
+++ b/src/app/user/model/User.js
@@ -0,0 +1,18 @@
+import mongoose from 'mongoose';
+
+const userSchema = new mongoose.Schema({
+ userid: { type: String, required: true, unique: true },
+ password: { type: String, required: true },
+ nickname: { type: String, required: true },
+ username: { type: String, required: true },
+ phone: { type: String, required: true },
+ address: { type: String, required: true },
+ email: { type: String, required: true, unique: true },
+ createdAt: { type: Date, default: Date.now }
+});
+
+// 연결된 DB가
+const User = mongoose.models.user || mongoose.model('user', userSchema, 'account');
+
+export default User;
+
diff --git a/src/app/util/chat.js b/src/app/util/chat.js
new file mode 100644
index 0000000..db57672
--- /dev/null
+++ b/src/app/util/chat.js
@@ -0,0 +1,65 @@
+// pages/chat.js
+import { useEffect, useState } from 'react';
+import io from 'socket.io-client';
+
+let socket;
+
+const Chat = () => {
+ const [username, setUsername] = useState('');
+ const [message, setMessage] = useState('');
+ const [messages, setMessages] = useState([]);
+
+ useEffect(() => {
+ // 서버와 소켓 연결
+ socket = io({
+ path: '/api/socket_io',
+ });
+
+ socket.on('message', (data) => {
+ setMessages((prev) => [...prev, data]);
+ });
+
+ return () => {
+ socket.disconnect();
+ };
+ }, []);
+
+ const sendMessage = () => {
+ if (username && message) {
+ socket.emit('message', {
+ user: username,
+ text: message,
+ timestamp: new Date().toISOString(),
+ });
+ setMessage('');
+ }
+ };
+
+ return (
+
+
실시간 채팅
+
+ {messages.map((msg, idx) => (
+
+ {msg.user} ({new Date(msg.timestamp).toLocaleTimeString()}): {msg.text}
+
+ ))}
+
+
setUsername(e.target.value)}
+ />
+
setMessage(e.target.value)}
+ />
+
전송
+
+ );
+};
+
+export default Chat;
diff --git a/src/app/util/socket.js b/src/app/util/socket.js
new file mode 100644
index 0000000..7a1818c
--- /dev/null
+++ b/src/app/util/socket.js
@@ -0,0 +1,25 @@
+// pages/api/socket.js
+import { Server as HTTPServer } from 'http';
+import { Server as SocketIOServer } from 'socket.io';
+
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+};
+
+export default function handler(req, res) {
+ if (!res.socket.server.io) {
+ const io = new SocketIOServer(res.socket.server, {
+ path: '/api/socket_io',
+ });
+ res.socket.server.io = io;
+
+ io.on('connection', (socket) => {
+ socket.on('message', (msg) => {
+ io.emit('message', msg);
+ });
+ });
+ }
+ res.end();
+}
diff --git a/src/constants/simpleDB.js b/src/constants/simpleDB.js
new file mode 100644
index 0000000..2c23e50
--- /dev/null
+++ b/src/constants/simpleDB.js
@@ -0,0 +1,11 @@
+const category_lists=
+ [
+ {key:'0', value:'ingredient', name:'식재료'},
+ {key:'1', value:'instant', name:'간편식/냉동식품'},
+ {key:'2', value:'stuffs', name:'생활용품'},
+ {key:'3', value:'large', name:'대용량'},
+ {key:'4', value:'deliver', name:'배달음식'},
+ {key:'5', value:'donate', name:'나눔템'},
+ ];
+
+export default category_lists;
diff --git a/src/lib/dbConnect.js b/src/lib/dbConnect.js
new file mode 100644
index 0000000..2a47cf0
--- /dev/null
+++ b/src/lib/dbConnect.js
@@ -0,0 +1,35 @@
+import mongoose from 'mongoose';
+
+const MONGODB_URI = process.env.MONGODB_URI;
+
+if (!MONGODB_URI) {
+ throw new Error('MONGODB_URI 환경 변수를 설정해주세요.');
+}
+
+let cached = global.mongoose;
+
+if (!cached) {
+ cached = global.mongoose = { conn: null, promise: null };
+}
+
+async function dbConnect() {
+ if (cached.conn) return cached.conn;
+
+ if (!cached.promise) {
+ cached.promise = mongoose.connect(process.env.MONGODB_URI).then(mongoose => {
+ mongoose.models = {};
+ return mongoose;
+ });
+ }
+
+ try {
+ cached.conn = await cached.promise;
+ } catch (e) {
+ cached.promise = null;
+ throw e;
+ }
+
+ return cached.conn;
+}
+
+export default dbConnect;
\ No newline at end of file
diff --git a/src/lib/session.js b/src/lib/session.js
new file mode 100644
index 0000000..b0c539a
--- /dev/null
+++ b/src/lib/session.js
@@ -0,0 +1,29 @@
+const { sealData, unsealData } = require('iron-session');
+
+const sessionSecret = process.env.SESSION_SECRET;
+
+if (!sessionSecret) {
+ throw new Error('SESSION_SECRET 환경변수가 필요해요!');
+}
+
+// 세션 암호화
+async function encrypt(data) {
+ return await sealData(data, {
+ password: sessionSecret,
+ ttl: 60 * 60 * 24 * 7, // 7일
+ });
+}
+
+// 세션 복호화
+async function decrypt(session) {
+ try {
+ return await unsealData(session, {
+ password: sessionSecret,
+ });
+ } catch (err) {
+ console.error('세션 복호화 실패:', err);
+ return null;
+ }
+}
+
+module.exports = { encrypt, decrypt };