diff --git a/.github/workflows/auto-merge-develop-pr.yml b/.github/workflows/auto-merge-develop-pr.yml
index daec7f9..86de8d7 100644
--- a/.github/workflows/auto-merge-develop-pr.yml
+++ b/.github/workflows/auto-merge-develop-pr.yml
@@ -1,23 +1,32 @@
-name: Auto Merge PR from Develop to Main
+name: Close Linked Issues if Checklist Complete
on:
pull_request:
- branches:
- - main
- types: [opened, synchronize, reopened]
+ types: [closed]
permissions:
- contents: write
- pull-requests: write
+ issues: write
+ pull-requests: read
+ contents: read
jobs:
- auto-merge:
- if: github.event.pull_request.head.ref == 'develop'
+ close-linked-issues:
+ if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
+
steps:
- - name: Enable auto-merge
- uses: peter-evans/enable-pull-request-automerge@v3
+ - name: Check if checklist is fully complete
+ id: checklist
+ run: |
+ BODY="${{ github.event.pull_request.body }}"
+ UNCHECKED=$(echo "$BODY" | grep -c '\[ \]' || true)
+ if [ "$UNCHECKED" -eq 0 ]; then
+ echo "checklist-complete=true" >> $GITHUB_OUTPUT
+ else
+ echo "checklist-complete=false" >> $GITHUB_OUTPUT
+ fi
+ - name: Close linked issues if checklist is complete
+ if: steps.checklist.outputs.checklist-complete == 'true'
+ uses: peter-evans/close-issue@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
- merge-method: merge
- pull-request-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/closed-issue.yml b/.github/workflows/closed-issue.yml
new file mode 100644
index 0000000..375418c
--- /dev/null
+++ b/.github/workflows/closed-issue.yml
@@ -0,0 +1,41 @@
+name: Close Mentioned Issues if Checklist Complete
+
+on:
+ pull_request:
+ types: [closed]
+
+permissions:
+ issues: write
+ pull-requests: read
+ contents: read
+
+jobs:
+ close-mentioned-issues:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check if checklist is fully complete
+ id: checklist
+ run: |
+ BODY="${{ github.event.pull_request.body }}"
+ UNCHECKED=$(echo "$BODY" | grep -c '\[ \]')
+ if [ "$UNCHECKED" -eq 0 ]; then
+ echo "checklist-complete=true" >> $GITHUB_OUTPUT
+ else
+ echo "checklist-complete=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Extract issue numbers
+ id: issues
+ run: |
+ BODY="${{ github.event.pull_request.body }}"
+ echo "ISSUES=$(echo "$BODY" | grep -oE '#[0-9]+' | tr -d '#' | tr '\n' ' ')" >> $GITHUB_OUTPUT
+
+ - name: Close mentioned issues
+ if: steps.checklist.outputs.checklist-complete == 'true'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ for issue in ${{ steps.issues.outputs.ISSUES }}; do
+ gh issue close "$issue" --repo "${{ github.repository }}"
+ done
diff --git a/.gitignore b/.gitignore
index a547bf3..3b0b403 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+.env
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index e7a3fb4..714014e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
},
"devDependencies": {
"@eslint/js": "^9.25.0",
+ "@types/node": "^22.15.24",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-router-dom": "^5.3.3",
@@ -1269,6 +1270,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "22.15.24",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz",
+ "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
"node_modules/@types/react": {
"version": "19.1.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz",
@@ -3280,6 +3291,13 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/package.json b/package.json
index 7c109d4..6b2d3a1 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
},
"devDependencies": {
"@eslint/js": "^9.25.0",
+ "@types/node": "^22.15.24",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-router-dom": "^5.3.3",
diff --git a/public/_redirects b/public/_redirects
new file mode 100644
index 0000000..f824337
--- /dev/null
+++ b/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
\ No newline at end of file
diff --git "a/public/images/characters/\354\225\231\352\270\200\354\235\264.png" "b/public/images/characters/\354\225\231\352\270\200\354\235\264.png"
new file mode 100644
index 0000000..04a4cfd
Binary files /dev/null and "b/public/images/characters/\354\225\231\352\270\200\354\235\264.png" differ
diff --git "a/public/images/characters/\354\233\205\354\235\264.png" "b/public/images/characters/\354\233\205\354\235\264.png"
new file mode 100644
index 0000000..6699d14
Binary files /dev/null and "b/public/images/characters/\354\233\205\354\235\264.png" differ
diff --git "a/public/images/characters/\355\213\260\353\260\224\353\205\270.png" "b/public/images/characters/\355\213\260\353\260\224\353\205\270.png"
new file mode 100644
index 0000000..9c0c4b7
Binary files /dev/null and "b/public/images/characters/\355\213\260\353\260\224\353\205\270.png" differ
diff --git a/src/App.tsx b/src/App.tsx
index 6c80a53..3d55f46 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,6 +10,7 @@ import SettingPage from "./pages/chatting/SettingPage";
import ChatPage from "./pages/chatting/ChatPage";
import styled from "styled-components";
import WritingPage from "./pages/writing/WritingPage";
+import Testpage from "./pages/Testpage";
function App() {
return (
@@ -19,13 +20,14 @@ function App() {
} />
} />
} />
- } />
+ } />
} />
} />
} />
} />
} />
+ } />
diff --git a/src/components/home/DiaryList.tsx b/src/components/home/DiaryList.tsx
index ebdbba6..1735393 100644
--- a/src/components/home/DiaryList.tsx
+++ b/src/components/home/DiaryList.tsx
@@ -1,49 +1,48 @@
+import { useEffect, useState } from "react";
import styled from "styled-components";
+import { getAllDiary } from "../../services/apis/diary/diary";
+import { useNavigate } from "react-router-dom";
-const diaries = [
- {
- date: "2025.05.01.",
- title: "오늘은 영화 보러 간 날",
- tags: ["#취미", "#휴식"],
- content: "재밌었다!".repeat(30),
- },
- {
- date: "2025.05.01.",
- title: "오늘은 영화 보러 간 날",
- tags: ["#취미", "#휴식"],
- content: "재밌었다!".repeat(30),
- },
- {
- date: "2025.05.01.",
- title: "오늘은 영화 보러 간 날",
- tags: ["#취미", "#휴식"],
- content: "재밌었다!".repeat(30),
- },
- {
- date: "2025.05.01.",
- title: "오늘은 영화 보러 간 날",
- tags: ["#취미", "#휴식"],
- content: "재밌었다!".repeat(30),
- },
- {
- date: "2025.05.01.",
- title: "오늘은 영화 보러 간 날",
- tags: ["#취미", "#휴식"],
- content: "재밌었다!".repeat(30),
- },
-];
+interface Diary {
+ id: number;
+ date: string;
+ title: string;
+ hashTags: string[];
+ content: string;
+}
const DiaryList = () => {
+ const [diaries, setDiaries] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ getAllDiary()
+ .then((data) => {
+ setDiaries(data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error("일기 불러오기 실패:", err);
+ setError("일기를 불러오지 못했습니다.");
+ setLoading(false);
+ });
+ }, []);
+
+ if (loading) return 로딩 중...
;
+ if (error) return {error}
;
+
return (
- {diaries.map((diary, index) => (
-
+ {diaries.map((diary) => (
+ navigate(`/diary/${diary.id}`)}>
{diary.date}
{diary.title}
- {diary.tags.map((tag, i) => (
- {tag}
+ {diary.hashTags.map((tag, i) => (
+ #{tag}
))}
{diary.content}
diff --git a/src/components/login/Login.tsx b/src/components/login/Login.tsx
index 86f9f38..4b33738 100644
--- a/src/components/login/Login.tsx
+++ b/src/components/login/Login.tsx
@@ -3,7 +3,7 @@ import styled from "styled-components";
const Login = () => {
// const REST_API_KEY = 'b57e43877ad662a5a26d85d3b6ff834e';
// const REDIRECT_URI = 'http://localhost:5173/oauth/kakao/callback';
- const link = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=b57e43877ad662a5a26d85d3b6ff834e&redirect_uri=http://localhost:5173/oauth/kakao/callback`;
+ const link = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=b57e43877ad662a5a26d85d3b6ff834e&redirect_uri=https://withsoulmate.netlify.app/oauth/kakao/callback`;
const loginHandler = () => {
window.location.href = link;
diff --git a/src/pages/DiaryDetail.tsx b/src/pages/DiaryDetail.tsx
index e4e7093..ca72115 100644
--- a/src/pages/DiaryDetail.tsx
+++ b/src/pages/DiaryDetail.tsx
@@ -1,51 +1,93 @@
import styled from "styled-components";
import { IoHomeOutline } from "react-icons/io5";
import { BsPencil } from "react-icons/bs";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { getDiary } from "../services/apis/diary/diary";
+import { generateAIComment } from "../services/gpt/openai";
+
+interface DiaryResponse {
+ id: number;
+ date: string;
+ title: string;
+ content: string;
+ comment: string;
+ character: string;
+ hashTags: string[];
+}
const DiaryDetail = () => {
const navigate = useNavigate();
+ const { diaryId } = useParams();
+
+ const [diary, setDiary] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [aiComment, setAiComment] = useState(null);
+
+ useEffect(() => {
+ if (diaryId) {
+ getDiary(diaryId)
+ .then((data) => {
+ setDiary(data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error("Error fetching diary:", err);
+ setError("일기를 불러오는 데 실패했습니다.");
+ setLoading(false);
+ });
+ }
+ }, [diaryId]);
+
+ useEffect(() => {
+ if (diary) {
+ generateAIComment(diary.content, diary.title)
+ .then((comment) => setAiComment(comment))
+ .catch(() => setAiComment("AI 코멘트를 생성하는 데 실패했습니다."));
+ }
+ }, [diary]);
+
+ const formatDate = (rawDate: string) => {
+ const date = new Date(rawDate);
+ return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, "0")}.${String(date.getDate()).padStart(2, "0")}.`;
+ };
+
+ if (loading) return 로딩 중...
;
+ if (error) return {error}
;
+ if (!diary) return 일기 데이터가 없습니다.
;
return (
navigate("/")} />
- 2025.05.01.
-
+ {formatDate(diary.date)}
+ navigate(`/edit/${diary.id}`)} />
- 오늘은 영화 보러 간 날
+ {diary.title}
- #취미
- #휴식
+ {diary.hashTags && diary.hashTags.length > 0 ? (
+ diary.hashTags.map((tag, idx) => #{tag})
+ ) : (
+ #태그없음
+ )}
-
- 오늘은 오랜만에 영화관에 가서 영화를 보고 왔다. 요즘 바빠서 제대로
- 쉬지도 못했는데, 이렇게 여유롭게 시간을 보내니 기분이 참 좋았다.
- 친구와 약속을 잡고 미리 예매까지 해두었는데, 다행히 좋은 자리에서
- 관람할 수 있었다.
-
- 보고 싶었던 영화라 기대가 컸는데, 그 기대를 저버리지 않고 정말
- 재미있었다. 배우들의 연기도 훌륭했고, 스토리도 탄탄해서 중간에 지루할
- 틈이 없었다. 영화관 특유의 분위기, 어두운 조명과 커다란 스크린, 웅장한
- 사운드까지 모든 게 몰입감을 더해줬다.
-
- 팝콘과 콜라도 빠질 수 없어서, 먹으면서 영화 보는 재미도 쏠쏠했다. 엔딩
- 크레딧이 올라갈 때는 아쉬운 마음도 들었지만, 오랜만에 힐링되는 시간을
- 보낸 것 같아 만족스럽다. 다음엔 다른 장르의 영화도 보러 가고 싶다.
-
+ {diary.content}
AI 친구의 코멘트
-
- 웅이
+
+ {diary.character}
- 오랜만에 영화관에서 좋은 시간 보냈다니 내가 다 기쁘다! 너의 여유로운
- 하루가 참 따뜻하게 느껴져 :)
+ {aiComment || "AI 코멘트를 생성 중입니다..."}
diff --git a/src/pages/Testpage.tsx b/src/pages/Testpage.tsx
new file mode 100644
index 0000000..f607683
--- /dev/null
+++ b/src/pages/Testpage.tsx
@@ -0,0 +1,30 @@
+import { useState } from "react";
+import { sendMessageToGPT } from "../services/gpt/openai";
+
+const Testpage = () => {
+ const [input, setInput] = useState("");
+ const [response, setResponse] = useState("");
+
+ const handleSubmit = async () => {
+ const result = await sendMessageToGPT(input);
+ setResponse(result);
+ };
+
+ return (
+
+ );
+};
+
+export default Testpage;
diff --git a/src/pages/chatting/ChatPage.tsx b/src/pages/chatting/ChatPage.tsx
index e6b51ea..d79e8cf 100644
--- a/src/pages/chatting/ChatPage.tsx
+++ b/src/pages/chatting/ChatPage.tsx
@@ -81,7 +81,9 @@ const EndChatButton = styled.button`
const ChatPage = () => {
const { chatId = "", character = "" } = useParams();
- const [messages, setMessages] = useState<{ role: string; content: string }[]>([]);
+ const [messages, setMessages] = useState<{ role: string; content: string }[]>(
+ [],
+ );
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const { selectedDate } = useSettingStore();
@@ -99,13 +101,28 @@ const ChatPage = () => {
const reply = await postComment(chatId, character, userMessage.content);
if (reply?.content) {
- setMessages((prev) => [...prev, { role: "bot", content: reply.content }]);
+ setMessages((prev) => [
+ ...prev,
+ { role: "bot", content: reply.content },
+ ]);
} else {
- setMessages((prev) => [...prev, { role: "bot", content: "응답을 받지 못했습니다. 다시 시도해 주세요." }]);
+ setMessages((prev) => [
+ ...prev,
+ {
+ role: "bot",
+ content: "응답을 받지 못했습니다. 다시 시도해 주세요.",
+ },
+ ]);
}
} catch (error) {
console.error("Failed to send message:", error);
- setMessages((prev) => [...prev, { role: "bot", content: "에러가 발생했습니다. 나중에 다시 시도해 주세요." }]);
+ setMessages((prev) => [
+ ...prev,
+ {
+ role: "bot",
+ content: "에러가 발생했습니다. 나중에 다시 시도해 주세요.",
+ },
+ ]);
} finally {
setLoading(false);
}
@@ -125,9 +142,10 @@ const ChatPage = () => {
}
try {
- const formattedDate = selectedDate instanceof Date
- ? selectedDate.toISOString().split("T")[0]
- : selectedDate;
+ const formattedDate =
+ selectedDate instanceof Date
+ ? selectedDate.toISOString().split("T")[0]
+ : selectedDate;
const response = await postDiary(chatId, character, formattedDate);
alert("대화가 저장되었습니다.");
diff --git a/src/pages/collection/Comments.tsx b/src/pages/collection/Comments.tsx
index 4457a87..1de4d7f 100644
--- a/src/pages/collection/Comments.tsx
+++ b/src/pages/collection/Comments.tsx
@@ -1,9 +1,9 @@
const Comments = () => {
return (
-
-
Comments
+ <>
+ <>Comments>
일단 없는 걸로
-
+ >
);
};
export default Comments;
diff --git a/src/pages/collection/Hashtags.tsx b/src/pages/collection/Hashtags.tsx
index d2d79cf..e8e96d1 100644
--- a/src/pages/collection/Hashtags.tsx
+++ b/src/pages/collection/Hashtags.tsx
@@ -1,9 +1,9 @@
const Hashtags = () => {
return (
-
-
Hashtags
+ <>
+ <>Hashtags>
일단 없는 걸로
-
+ >
);
};
export default Hashtags;
diff --git a/src/pages/writing/WritingPage.tsx b/src/pages/writing/WritingPage.tsx
index 9a7d41d..6eace6d 100644
--- a/src/pages/writing/WritingPage.tsx
+++ b/src/pages/writing/WritingPage.tsx
@@ -98,7 +98,7 @@ const TextInput = styled.input`
outline: none;
&::placeholder {
- color: #B0BCD2;
+ color: #b0bcd2;
}
`;
@@ -110,7 +110,7 @@ const TagInput = styled.input`
border-radius: 12px;
outline: none;
&::placeholder {
- color: #B0BCD2;
+ color: #b0bcd2;
}
`;
@@ -127,6 +127,6 @@ const ContentArea = styled.textarea`
outline: none;
&::placeholder {
- color: #6D7EA0;
+ color: #6d7ea0;
}
`;
diff --git a/src/services/apis/chatting/chat.ts b/src/services/apis/chatting/chat.ts
index 46d817c..7c1e5a1 100644
--- a/src/services/apis/chatting/chat.ts
+++ b/src/services/apis/chatting/chat.ts
@@ -62,14 +62,11 @@ export async function postDiary(
date: string,
) {
try {
- const response = await axiosInstanceWithToken.post(
- `https://soulmate.o-r.kr/api/diary/generate`,
- {
- chatId: chatId,
- character: character,
- date: date,
- },
- );
+ const response = await axiosInstanceWithToken.post(`/api/diary/generate`, {
+ chatId: chatId,
+ character: character,
+ date: date,
+ });
return response.data;
} catch (error) {
throw error;
diff --git a/src/services/apis/diary/diary.ts b/src/services/apis/diary/diary.ts
new file mode 100644
index 0000000..fa48b62
--- /dev/null
+++ b/src/services/apis/diary/diary.ts
@@ -0,0 +1,23 @@
+import { CreateAxiosInstanceWithToken } from "../axiosInstanceWithToken";
+
+const axiosInstanceWithToken = CreateAxiosInstanceWithToken();
+
+export async function getAllDiary() {
+ try {
+ const response = await axiosInstanceWithToken.get(`/api/diary/get`);
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function getDiary(diaryId: string) {
+ try {
+ const response = await axiosInstanceWithToken.get(
+ `/api/diary/get/${diaryId}`,
+ );
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
diff --git a/src/services/gpt/openai.ts b/src/services/gpt/openai.ts
new file mode 100644
index 0000000..80257cb
--- /dev/null
+++ b/src/services/gpt/openai.ts
@@ -0,0 +1,69 @@
+import axios from "axios";
+
+const API_KEY = import.meta.env.VITE_OPENAI_API_KEY;
+const API_URL = "https://api.openai.com/v1/chat/completions";
+
+export const sendMessageToGPT = async (message: string) => {
+ try {
+ const response = await axios.post(
+ API_URL,
+ {
+ model: "gpt-3.5-turbo",
+ messages: [{ role: "user", content: message }],
+ },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${API_KEY}`,
+ },
+ },
+ );
+
+ return response.data.choices[0].message.content;
+ } catch (error) {
+ console.error("GPT API 호출 오류:", error);
+ throw error;
+ }
+};
+
+/**
+ * GPT에게 일기 내용을 기반으로 따뜻한 코멘트를 요청합니다.
+ * @param content 일기 내용
+ * @param title 일기 제목
+ * @returns GPT가 생성한 AI 코멘트 문자열
+ */
+export const generateAIComment = async (
+ content: string,
+ title: string,
+): Promise => {
+ const prompt = `
+너는 따뜻하고 공감 능력이 뛰어난 AI 친구야. 사용자가 작성한 아래의 일기를 읽고,
+부드럽고 진심 어린 말투 반말로 짧은 코멘트를 작성해줘.
+
+제목: ${title}
+내용: ${content}
+
+AI 코멘트:
+`;
+
+ try {
+ const response = await axios.post(
+ API_URL,
+ {
+ model: "gpt-3.5-turbo",
+ messages: [{ role: "user", content: prompt }],
+ },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${API_KEY}`,
+ },
+ },
+ );
+
+ return response.data.choices[0].message.content.trim();
+ } catch (error) {
+ console.error("GPT API 호출 오류:", error);
+ throw new Error("AI 코멘트를 생성하는 데 실패했습니다.");
+ }
+};