Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 14 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
run: chmod +x gradlew

- name: Gradle 빌드 수행
env:
KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
run: ./gradlew clean assemble --info

- name: 빌드 아티팩트 업로드
Expand Down Expand Up @@ -106,7 +111,7 @@ jobs:
echo "Generating .env.properties file..."

# 1. Secrets 변수 주입
echo "jwt.secret.key='${{ secrets.JWT_SECRET_KEY }}'" >> .env.properties
echo "jwt.secret.key='${{ secrets.SECRET_KEY }}'" >> .env.properties
echo "kakao.client.id=${{ secrets.KAKAO_CLIENT_ID }}" >> .env.properties
echo "mail.password='${{ secrets.MAIL_PASSWORD }}'" >> .env.properties
echo "mail.username=${{ secrets.MAIL_USERNAME }}" >> .env.properties
Expand All @@ -119,9 +124,16 @@ jobs:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/pinco
SPRING_DATASOURCE_USERNAME: user
SPRING_DATASOURCE_PASSWORD: password
# Redis(Spring Boot 기본 설정 사용 시)

SPRING_DATA_REDIS_HOST: localhost
SPRING_DATA_REDIS_PORT: 6379

# 환경 변수 전달 (메일/카카오/시크릿 등)
KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}

run: ./gradlew test --info

- name: 테스트 결과를 PR에 코멘트로 등록
Expand Down
108 changes: 95 additions & 13 deletions front/src/app/user/join/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,59 @@

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Mail, Lock, User } from "lucide-react";
import { apiJoin } from "@/lib/pincoApi";
import { Mail, Lock, User, Key } from "lucide-react";
import { apiJoin, apiSendVerificationCode } from "@/lib/pincoApi";

export default function SignUpPage() {
const router = useRouter();
const [form, setForm] = useState({ userName: "", email: "", password: "" });
const [form, setForm] = useState({ userName: "", email: "", password: "", verificationCode: "" });
const [loading, setLoading] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
const [codeSent, setCodeSent] = useState(false);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};

const handlePasswordBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const password = e.target.value.trim();
if (password && password.length < 8) {
alert("비밀번호는 8자 이상 입력해야 합니다.");
}
};

const handleSendVerificationCode = async () => {
const email = form.email.trim().toLowerCase();

if (!email) {
alert("이메일을 먼저 입력해주세요.");
return;
}

// 이메일 형식 간단 검증
if (!email.includes("@") || !email.includes(".")) {
alert("올바른 이메일 형식을 입력해주세요.");
return;
}

setSendingCode(true);
try {
await apiSendVerificationCode(email);
alert("인증코드가 발송되었습니다. 이메일을 확인해주세요.");
setCodeSent(true);
} catch (err: any) {
const msg = err?.message ?? "";
if (msg.includes("이메일 형식")) {
alert("이메일 형식이 올바르지 않습니다.");
} else {
alert(msg || "인증코드 발송에 실패했습니다.");
}
} finally {
setSendingCode(false);
}
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (loading) return;
Expand All @@ -23,6 +63,7 @@ export default function SignUpPage() {
const email = form.email.trim().toLowerCase();
const password = form.password.trim();
const userName = form.userName.trim();
const verificationCode = form.verificationCode.trim();

// ⚙️ 프론트 최소 유효성
if (password.length < 8) {
Expand All @@ -31,9 +72,15 @@ export default function SignUpPage() {
return;
}

if (!verificationCode) {
alert("인증코드를 입력해주세요.");
setLoading(false);
return;
}

try {
// ⚙️ 서버 요청
const res: any = await apiJoin(email, password, userName);
const res: any = await apiJoin(email, password, userName, verificationCode);
const code = res?.resultCode ?? res?.errorCode ?? "200";
const msg = res?.msg ?? "";

Expand Down Expand Up @@ -78,6 +125,8 @@ export default function SignUpPage() {
alert("이미 가입된 이메일입니다. 로그인해주세요.");
else if (raw.includes("이미 사용 중인 회원이름"))
alert("이미 사용 중인 회원 이름입니다. 다른 이름을 입력해주세요.");
else if (raw.includes("인증 코드") || raw.includes("인증코드"))
alert(raw || "인증코드가 일치하지 않거나 만료되었습니다.");
else alert(raw || "회원가입 중 오류가 발생했습니다 ❌");
} finally {
setLoading(false);
Expand Down Expand Up @@ -111,17 +160,49 @@ export default function SignUpPage() {
{/* 이메일 */}
<div className="relative">
<Mail className="absolute left-3 top-3 text-gray-400" size={18} />
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="이메일 주소"
required
className="w-full border rounded-md pl-10 pr-3 py-2 focus:ring-2 focus:ring-blue-500"
/>
<div className="flex gap-2">
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="이메일 주소"
required
className="flex-1 border rounded-md pl-10 pr-3 py-2 focus:ring-2 focus:ring-blue-500"
disabled={codeSent}
/>
<button
type="button"
onClick={handleSendVerificationCode}
disabled={sendingCode || codeSent || !form.email}
className={`px-4 py-2 rounded-md text-sm whitespace-nowrap transition
${sendingCode || codeSent || !form.email
? "bg-gray-300 text-gray-500 cursor-not-allowed"
: "bg-blue-500 text-white hover:bg-blue-600"
}`}
>
{sendingCode ? "발송 중..." : codeSent ? "발송 완료" : "인증번호 전송"}
</button>
</div>
</div>

{/* 인증코드 */}
{codeSent && (
<div className="relative">
<Key className="absolute left-3 top-3 text-gray-400" size={18} />
<input
type="text"
name="verificationCode"
value={form.verificationCode}
onChange={handleChange}
placeholder="인증코드 6자리 입력"
maxLength={6}
required
className="w-full border rounded-md pl-10 pr-3 py-2 focus:ring-2 focus:ring-blue-500"
/>
</div>
)}

{/* 비밀번호 */}
<div className="relative">
<Lock className="absolute left-3 top-3 text-gray-400" size={18} />
Expand All @@ -130,6 +211,7 @@ export default function SignUpPage() {
name="password"
value={form.password}
onChange={handleChange}
onBlur={handlePasswordBlur}
placeholder="비밀번호 (8자 이상)"
minLength={8}
required
Expand Down
4 changes: 2 additions & 2 deletions front/src/app/user/mypage/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export default function EditMyInfoPage() {
return;
}

// ✅ 서버에 보낼 payload 구성
const payload: Record<string, unknown> = { password };
// ✅ 서버에 보낼 payload 구성 (newUserName, newPassword는 선택적 - 값이 있을 때만 전송)
const payload: Record<string, string> = { password };
if (newUserName.trim()) payload.newUserName = newUserName.trim();
if (newPassword) payload.newPassword = newPassword;

Expand Down
55 changes: 6 additions & 49 deletions front/src/hooks/usePins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,33 +267,13 @@ export function usePins(initialCenter: UsePinsProps, userId?: number | null) {
return;
}

const apiKey = localStorage.getItem("apiKey");
const accessToken = localStorage.getItem("accessToken");

if (!apiKey || !accessToken) {
console.error("❌ 토큰이 없습니다. 로그인이 필요합니다.");
alert("로그인이 필요합니다.");
return;
}

setLoading(true);
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/bookmarks`, {
const data = await fetchApi<any>("/api/bookmarks", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey} ${accessToken}`,
},
credentials: "include",
});

if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}

const data = await res.json();

const pinsOnly = extractArray(data.data).map((b: any) => b.pin ?? b);
const pinsOnly = extractArray(data).map((b: any) => b.pin ?? b);
const normalized = normalizePins(pinsOnly);

const pinsWithTags = await loadTagsForPins(normalized);
Expand Down Expand Up @@ -324,36 +304,13 @@ export function usePins(initialCenter: UsePinsProps, userId?: number | null) {
return;
}

const apiKey = localStorage.getItem("apiKey");
const accessToken = localStorage.getItem("accessToken");

if (!apiKey || !accessToken) {
console.error("❌ 토큰이 없습니다. 로그인이 필요합니다.");
alert("로그인이 필요합니다.");
return;
}

setLoading(true);
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/${userId}/likespins`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey} ${accessToken}`,
},
credentials: "include",
}
);

if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}

const data = await res.json();
const data = await fetchApi<any>(`/api/user/${userId}/likespins`, {
method: "GET",
});

const likedArray = extractArray(data.data);
const likedArray = extractArray(data);
const normalized = normalizePins(likedArray);

const pinsWithTags = await loadTagsForPins(normalized);
Expand Down
29 changes: 13 additions & 16 deletions front/src/lib/pincoApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,33 +118,23 @@ export const apiDeletePin = (id: number) =>

// ✅ 좋아요 추가
export const apiAddLike = async (pinId: number, userId: number) => {
const apiKey = localStorage.getItem("apiKey");
const accessToken = localStorage.getItem("accessToken");

if (!apiKey || !accessToken) {
console.error("❌ 토큰이 없습니다. 로그인이 필요합니다.");
alert("로그인이 필요합니다.");
return;
}
const res:LikesStatusDto = await fetchApi(`/api/pins/${pinId}/likes`, {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey} ${accessToken}` },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
});

if (res) return res; // ✅ { data: { isLiked, likeCount } }
if (res) return res; // ✅ { data: { isLiked, likeCount } }
};

// ✅ 좋아요 취소
export const apiRemoveLike = async (pinId: number, userId: number) => {
const apiKey = localStorage.getItem("apiKey");
const accessToken = localStorage.getItem("accessToken");
const res:LikesStatusDto = await fetchApi(`/api/pins/${pinId}/likes`, {
method: "DELETE",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey} ${accessToken}` },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
});
if (res) return res; // ✅ { data: { isLiked, likeCount } }
if (res) return res; // ✅ { data: { isLiked, likeCount } }
};

export const apiGetLikeUsers = (pinId: number) =>
Expand Down Expand Up @@ -174,11 +164,18 @@ export const apiDeleteBookmark = (bookmarkId: number) => {
};

// ---------- User ----------
export const apiJoin = (email: string, password: string, userName: string) =>
export const apiSendVerificationCode = (email: string) =>
fetchApi<void>(`/api/user/send-verification-code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});

export const apiJoin = (email: string, password: string, userName: string, verificationCode: string) =>
fetchApi<void>(`/api/user/join`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, userName }),
body: JSON.stringify({ email, password, userName, verificationCode }),
});

export const apiDeleteAccount = (password: string) =>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/back/pinco/domain/tag/entity/PinTag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener
indexes = [
Index(name = "idx_pin_tag_pin", columnList = "pin_id"),
Index(name = "idx_pin_tag_tag",columnList = "tag_id"),
Index(name = "idx_pin_tag_deleted", columnList = "is_deleted")
// Index(name = "idx_pin_tag_deleted", columnList = "is_deleted")
]
)
@EntityListeners(AuditingEntityListener::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ class UserController(
private val rq: Rq
) {


@Operation(summary = "인증코드 발송", description = "이메일로 6자리 인증코드를 발송합니다.")
@PostMapping("/send-verification-code")
fun sendVerificationCode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package com.back.pinco.domain.user.dto.UserReqBody

data class EditRequest(
val password: String, // 현재 비밀번호 (검증용)
val newUserName: String, // 변경할 닉네임
val newPassword: String // 변경할 비밀번호
val newUserName: String? = null, // 변경할 닉네임 (선택)
val newPassword: String? = null // 변경할 비밀번호 (선택)
)
Loading
Loading