Skip to content

Commit 5804358

Browse files
authored
feat: 관심사 수정 api 추가
feat: 관심사 수정 api 추가
2 parents 372cf43 + ea39857 commit 5804358

File tree

5 files changed

+148
-5
lines changed

5 files changed

+148
-5
lines changed

src/controllers/user.controller.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getMeByUserId,
55
saveOnboardingByUserId,
66
getMyInterestsByUserId,
7+
patchMyInterestsByUserId,
78
} from "../services/user.service.js";
89

910
import { requireAuth } from "../auth.config.js";
@@ -68,6 +69,21 @@ app.post("/api/users", async (req, res) => {
6869
}
6970
});
7071

72+
// PATCH /api/users/me/interests
73+
app.patch("/api/users/me/interests", requireAuth, async (req, res) => {
74+
console.log("auth userId:", req.auth?.userId, typeof req.auth?.userId);
75+
try {
76+
const result = await patchMyInterestsByUserId(req.auth.userId, req.body);
77+
return res.status(200).json(result);
78+
} catch (e) {
79+
console.error("[PATCH /api/users/me/interests] error:", e);
80+
return res
81+
.status(e.statusCode ?? 500)
82+
.json(e.payload ?? defaultInterestsPatchFail());
83+
}
84+
});
85+
86+
7187
// POST /api/users/me/onboarding
7288
app.post("/api/users/me/onboarding", requireAuth, async (req, res) => {
7389
console.log("[ONBOARDING] req.auth =", req.auth);
@@ -113,4 +129,12 @@ function defaultOnboardingFail() {
113129
error: { errorCode: "O500", reason: "온보딩 저장 중 서버 오류가 발생했습니다.", data: null },
114130
success: null,
115131
};
132+
}
133+
134+
function defaultInterestsPatchFail() {
135+
return {
136+
resultType: "FAIL",
137+
error: { errorCode: "I500", reason: "관심사 수정 중 서버 오류가 발생했습니다.", data: null },
138+
success: null,
139+
};
116140
}

src/dtos/user.dto.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,29 @@ export function validateOnboardingBody(body) {
4141

4242
return { goalPeriod, calendar, profile };
4343
}
44+
45+
/**
46+
* Interests PATCH DTO
47+
* body: { interestIds: number[] }
48+
*/
49+
export function validatePatchInterestsBody(body) {
50+
const interestIds = body?.interestIds;
51+
52+
if (!Array.isArray(interestIds)) {
53+
const err = new Error("interestIds must be array");
54+
err.statusCode = 400;
55+
err.payload = {
56+
resultType: "FAIL",
57+
error: { errorCode: "I400", reason: "interestIds는 배열이어야 합니다.", data: null },
58+
success: null,
59+
};
60+
throw err;
61+
}
62+
63+
// 숫자 배열로 정규화 (정수만)
64+
const normalized = interestIds
65+
.map((v) => Number(v))
66+
.filter((n) => Number.isInteger(n) && n > 0);
67+
68+
return { interestIds: normalized };
69+
}

src/repositories/user.repository.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export async function createUser({ email, provider, oauthId, name }) {
1212
}
1313

1414
export async function findUserById(id) {
15-
return prisma.user.findFirst({ where: { id } });
15+
return prisma.user.findFirst({ where: { id: Number(id) } });
1616
}
1717

1818
export async function markOnboardingCompleted(id) {
@@ -44,9 +44,9 @@ export async function createGoalPeriod({
4444
*/
4545
export async function upsertUserProfileInterests(userId, interests) {
4646
return prisma.userProfile.upsert({
47-
where: { userId },
47+
where: { userId: Number(userId) },
4848
update: { interests },
49-
create: { userId, interests },
49+
create: { userId: Number(userId), interests },
5050
});
5151
}
5252

src/services/user.service.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { verifyGoogleIdToken, signAccessToken, EXPIRES_IN_SECONDS, signRefreshToken,
22
REFRESH_EXPIRES_IN_SECONDS, } from "../auth.config.js";
3-
import { parseIdTokenBody, validateOnboardingBody } from "../dtos/user.dto.js";
3+
import { parseIdTokenBody, validateOnboardingBody, validatePatchInterestsBody } from "../dtos/user.dto.js";
44
import {
55
findUserByProviderOauthId,
66
createUser,
@@ -13,6 +13,7 @@ import {
1313
findInterestsByIds,
1414
} from "../repositories/user.repository.js";
1515

16+
1617
/**
1718
* POST /api/users/oauth2/google
1819
* - Request: { idToken }
@@ -267,4 +268,41 @@ function authFailGoogle() {
267268
success: null,
268269
};
269270
return err;
270-
}
271+
}
272+
273+
/**
274+
* PATCH /api/users/me/interests
275+
* - Request: { interestIds: number[] }
276+
* - Response: { interests: [{id, name}, ...] }
277+
*/
278+
export async function patchMyInterestsByUserId(userId, body) {
279+
const { interestIds } = validatePatchInterestsBody(body);
280+
281+
const user = await findUserById(userId);
282+
if (!user) {
283+
const err = new Error("User not found");
284+
err.statusCode = 404;
285+
err.payload = {
286+
resultType: "FAIL",
287+
error: { errorCode: "U404", reason: "사용자 정보를 찾을 수 없습니다.", data: null },
288+
success: null,
289+
};
290+
throw err;
291+
}
292+
293+
// 1) (선택) interest master에 존재하는 id만 남기기
294+
const existing = await findInterestsByIds(interestIds);
295+
const validIds = existing.map((x) => x.id);
296+
297+
// 2) userProfile.interests upsert
298+
await upsertUserProfileInterests(userId, validIds);
299+
300+
// 3) 최신 관심사 목록 반환
301+
return {
302+
resultType: "SUCCESS",
303+
error: null,
304+
success: {
305+
interests: existing, // id/name 형태로 반환
306+
},
307+
};
308+
}

src/swagger/user.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,61 @@ paths:
291291
"500":
292292
$ref: "#/components/responses/FailResponse"
293293

294+
patch:
295+
summary: 내 관심사 수정
296+
tags: [User]
297+
security:
298+
- bearerAuth: []
299+
requestBody:
300+
required: true
301+
content:
302+
application/json:
303+
schema:
304+
type: object
305+
required: [interestIds]
306+
properties:
307+
interestIds:
308+
type: array
309+
items:
310+
type: integer
311+
example: [1, 3, 8]
312+
responses:
313+
"200":
314+
description: 수정 성공
315+
content:
316+
application/json:
317+
schema:
318+
type: object
319+
properties:
320+
resultType:
321+
type: string
322+
example: "SUCCESS"
323+
error:
324+
nullable: true
325+
example: null
326+
success:
327+
type: object
328+
properties:
329+
interests:
330+
type: array
331+
items:
332+
type: object
333+
properties:
334+
id:
335+
type: integer
336+
example: 1
337+
name:
338+
type: string
339+
example: "운동"
340+
"400":
341+
$ref: "#/components/responses/FailResponse"
342+
"401":
343+
$ref: "#/components/responses/FailResponse"
344+
"404":
345+
$ref: "#/components/responses/FailResponse"
346+
"500":
347+
$ref: "#/components/responses/FailResponse"
348+
294349
/api/users:
295350
post:
296351
summary: 회원가입 (Google)

0 commit comments

Comments
 (0)