diff --git a/src/applications/applications.controller.js b/src/applications/applications.controller.js index bb3a398..d4b778d 100644 --- a/src/applications/applications.controller.js +++ b/src/applications/applications.controller.js @@ -202,9 +202,14 @@ export class ApplicationsController { * help: * id: 6 * helpType: 4 - * helpTypeText: "기타" + * helpTypeText: "기타 돌봄" * status: 0 * statusText: "요청" + * serviceDate: "2025-08-20T00:00:00.000Z" + * startTime: "1970-01-01T01:30:00.000Z" + * endTime: "1970-01-01T03:00:00.000Z" + * addressText: "서울시 동대문구 한교동" + * rewardTokens: 9 * totalApplicants: 0 * applicants: [] * pagination: @@ -219,9 +224,14 @@ export class ApplicationsController { * help: * id: 16 * helpType: 1 - * helpTypeText: "등하원" + * helpTypeText: "등하원 돌봄" * status: 0 * statusText: "요청" + * serviceDate: "2025-08-20T00:00:00.000Z" + * startTime: "1970-01-01T01:30:00.000Z" + * endTime: "1970-01-01T03:00:00.000Z" + * addressText: "서울시 동대문구 한교동" + * rewardTokens: 9 * totalApplicants: 1 * applicants: * - applicationId: 1 @@ -232,7 +242,7 @@ export class ApplicationsController { * helper: * id: 3 * nickname: "염둘" - * profileImageUrl: null + * imageUrl: null * reviewCount: 2 * ratingAvg: 5 * pagination: @@ -598,14 +608,16 @@ export class ApplicationsController { * help: * id: 20 * helpType: 1 - * helpTypeText: "등하원" + * helpTypeText: "등하원 돌봄" * serviceDate: "2025-08-20T00:00:00.000Z" * startTime: "1970-01-01T01:30:00.000Z" * endTime: "1970-01-01T03:00:00.000Z" + * addressText: "서울시 동대문구 한교동" + * rewardTokens: 9 * requester: * id: 5 * nickname: "김엄마" - * profileImageUrl: "https://example.com/profile.jpg" + * imageUrl: "https://example.com/profile.jpg" * reviewCount: 12 * ratingAvg: 4.8 * pagination: diff --git a/src/applications/applications.repository.js b/src/applications/applications.repository.js index 8d9cb49..2d7f603 100644 --- a/src/applications/applications.repository.js +++ b/src/applications/applications.repository.js @@ -24,6 +24,11 @@ export class ApplicationsRepository { requesterId: true, helpType: true, status: true, + serviceDate: true, + startTime: true, + endTime: true, + addressText: true, + rewardTokens: true, }, }); } @@ -213,6 +218,8 @@ export class ApplicationsRepository { serviceDate: true, startTime: true, endTime: true, + addressText: true, + rewardTokens: true, requester: { select: { id: true, diff --git a/src/applications/dto/applications.response.dto.js b/src/applications/dto/applications.response.dto.js index b64ab5b..492efaf 100644 --- a/src/applications/dto/applications.response.dto.js +++ b/src/applications/dto/applications.response.dto.js @@ -32,6 +32,11 @@ export class ApplyListResponseDto { helpTypeText: this._helpTypeText(help.helpType), status: help.status, statusText: this._helpStatusText(help.status), + serviceDate: help.serviceDate, + startTime: help.startTime, + endTime: help.endTime, + addressText: help.addressText, + rewardTokens: help.rewardTokens, }; // 전체 지원자 수 (페이지네이션 적용 전 총합) @@ -53,7 +58,7 @@ export class ApplyListResponseDto { } _helpTypeText(type) { - const map = { 1: "등하원", 2: "놀이", 3: "동행", 4: "기타" }; + const map = { 1: "등하원 돌봄", 2: "놀이 돌봄", 3: "동행 돌봄", 4: "기타 돌봄" }; return map[type] ?? "알 수 없음"; } _helpStatusText(status) { @@ -73,7 +78,7 @@ export class ApplicantListItemDto { this.helper = { id: app.helper?.id ?? app.userId, nickname: app.helper?.nickname ?? "알수없음", - profileImageUrl: + imageUrl: app.helper?.imageUrl || app.helper?.kakaoProfileImageUrl || null, reviewCount: stats.reviewCount ?? 0, ratingAvg: stats.ratingAvg ?? 0, @@ -101,10 +106,12 @@ export class MyApplicationItemDto { serviceDate: app.helpRequest.serviceDate, startTime: app.helpRequest.startTime, endTime: app.helpRequest.endTime, + addressText: app.helpRequest.addressText, + rewardTokens: app.helpRequest.rewardTokens, requester: { id: app.helpRequest.requester.id, nickname: app.helpRequest.requester.nickname, - profileImageUrl: + imageUrl: app.helpRequest.requester.imageUrl || app.helpRequest.requester.kakaoProfileImageUrl || null, @@ -120,7 +127,7 @@ export class MyApplicationItemDto { } _helpTypeText(type) { - const map = { 1: "등하원", 2: "놀이", 3: "동행", 4: "기타" }; + const map = { 1: "등하원 돌봄", 2: "놀이 돌봄", 3: "동행 돌봄", 4: "기타 돌봄" }; return map[type] ?? "알 수 없음"; } } \ No newline at end of file diff --git a/src/auth/auth.controller.js b/src/auth/auth.controller.js index 1d2b4ff..25647b1 100644 --- a/src/auth/auth.controller.js +++ b/src/auth/auth.controller.js @@ -475,8 +475,8 @@ import { AuthResponseDto } from "./dto/auth.response.dto.js"; // 로컬 개발용 const cookieOptsDev = { httpOnly: true, - secure: false, // http - sameSite: 'Lax', // 같은 PC 포트 간 요청만 + secure: true, // http + sameSite: 'None', // 같은 PC 포트 간 요청만 path: '/', maxAge: 7 * 24 * 60 * 60 * 1000, }; diff --git a/src/helps/dto/helps.request.dto.js b/src/helps/dto/helps.request.dto.js index 6faf60c..b97c72e 100644 --- a/src/helps/dto/helps.request.dto.js +++ b/src/helps/dto/helps.request.dto.js @@ -48,11 +48,14 @@ export class CreateHelpRequestDto { if (!this.serviceDate) { errors.push('서비스 날짜를 선택해주세요.'); } else { - const serviceDate = new Date(this.serviceDate); - const today = new Date(); - today.setHours(0, 0, 0, 0); - - if (serviceDate < today) { + // KST 기준으로 비교 → utils/time.js에서 헬퍼 불러와 사용 + // - toKstDateOnly: DB/입력 날짜를 KST 날짜 객체로 변환 + // - getKstStartOfTodayUtc: 오늘 00:00(KST)의 UTC (여기선 단순히 KST '오늘 00:00'을 만들어 비교) + const kstNow = new Date(new Date().toLocaleString("en-US", { timeZone: "Asia/Seoul" })); + kstNow.setHours(0, 0, 0, 0); // 오늘 KST 자정 + const serviceDateKst = new Date(new Date(this.serviceDate).toLocaleString("en-US", { timeZone: "Asia/Seoul" })); + serviceDateKst.setHours(0, 0, 0, 0); + if (serviceDateKst < kstNow) { errors.push('서비스 날짜는 오늘 또는 이후여야 합니다.'); } } diff --git a/src/helps/dto/helps.response.dto.js b/src/helps/dto/helps.response.dto.js index c322459..a052953 100644 --- a/src/helps/dto/helps.response.dto.js +++ b/src/helps/dto/helps.response.dto.js @@ -148,7 +148,7 @@ export class MyHelpRequestListItemDto { if (helpRequest.applications && helpRequest.applications.length > 0) { this.applicants = helpRequest.applications.map(app => ({ helperId: app.userId, - helperImageUrl: app.helper.imageUrl || app.helper.kakaoProfileImageUrl + imageUrl: app.helper.imageUrl || app.helper.kakaoProfileImageUrl })); } diff --git a/src/helps/helps.controller.js b/src/helps/helps.controller.js index da923de..a8d8944 100644 --- a/src/helps/helps.controller.js +++ b/src/helps/helps.controller.js @@ -55,7 +55,7 @@ export class HelpsController { * format: binary * description: "첨부 이미지 (선택사항)" * responses: - * 200: + * 201: * description: 돌봄요청 작성 성공 * content: * application/json: @@ -95,8 +95,8 @@ export class HelpsController { * type: string * example: "서비스 날짜는 오늘 또는 이후여야 합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -120,8 +120,8 @@ export class HelpsController { * type: string * example: "로그인이 필요합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -145,8 +145,8 @@ export class HelpsController { * type: string * example: "돌봄요청 생성 중 오류가 발생했습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -301,8 +301,8 @@ export class HelpsController { * type: string * example: "로그인이 필요합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -326,8 +326,8 @@ export class HelpsController { * type: string * example: "해당 돌봄요청을 찾을 수 없습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -451,8 +451,8 @@ export class HelpsController { * type: string * example: "서비스 날짜는 오늘 또는 이후여야 합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -476,8 +476,8 @@ export class HelpsController { * type: string * example: "로그인이 필요합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -501,8 +501,8 @@ export class HelpsController { * type: string * example: "자신의 돌봄요청만 수정할 수 있습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -526,8 +526,8 @@ export class HelpsController { * type: string * example: "해당 돌봄요청을 찾을 수 없습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -551,8 +551,8 @@ export class HelpsController { * type: string * example: "돌봄요청 수정 중 오류가 발생했습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -644,8 +644,8 @@ export class HelpsController { * type: string * example: "완료된 돌봄요청은 삭제할 수 없습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -669,8 +669,8 @@ export class HelpsController { * type: string * example: "로그인이 필요합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -694,8 +694,8 @@ export class HelpsController { * type: string * example: "자신의 돌봄요청만 삭제할 수 있습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -719,8 +719,8 @@ export class HelpsController { * type: string * example: "해당 돌봄요청을 찾을 수 없습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -744,8 +744,8 @@ export class HelpsController { * type: string * example: "돌봄요청 삭제 중 오류가 발생했습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -794,7 +794,7 @@ export class HelpsController { * schema: * type: integer * enum: [0, 1, 2] - * description: "매칭 상태 (0: 요청, 1: 배정, 2: 완료, 4: 모집종료)" + * description: "매칭 상태 (0: 요청, 1: 배정, 2: 완료)" * - in: query * name: helpTypes * schema: @@ -873,8 +873,8 @@ export class HelpsController { * type: string * example: "돌봄요청 조회 중 오류가 발생했습니다." * data: - * type: object - * example: {} + * nullable: true + * example: null * success: * nullable: true * example: null @@ -999,7 +999,7 @@ export class HelpsController { * helperId: * type: integer * example: 5 - * helperImageUrl: + * imageUrl: * type: string * example: "https://example.com/helper1.jpg" * assignedHelper: @@ -1047,8 +1047,8 @@ export class HelpsController { * type: string * example: "로그인이 필요합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -1072,8 +1072,8 @@ export class HelpsController { * type: string * example: "내 돌봄요청 조회 중 오류가 발생했습니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null @@ -1215,8 +1215,8 @@ export class HelpsController { * type: string * example: "로그인이 필요합니다." * data: - * type: object * nullable: true + * example: null * success: * nullable: true * example: null diff --git a/src/helps/helps.repository.js b/src/helps/helps.repository.js index 5eecc27..170e1c2 100644 --- a/src/helps/helps.repository.js +++ b/src/helps/helps.repository.js @@ -287,6 +287,79 @@ export class HelpsRepository { data: { status: 4, updatedAt: new Date() } }); } + + // 과거 날짜 → 모집종료(4) 처리 + async closeExpiredByPastDate(todayStartUtc) { + return prisma.helpRequest.updateMany({ + where: { status: 0, serviceDate: { lt: todayStartUtc } }, + data: { status: 4, updatedAt: new Date() }, + }); + } + + // (B) 오늘 + 시작시간 경과 → 모집종료(4) 처리 + async closeExpiredByStartTimeToday({ todayStartUtc, tomorrowStartUtc, nowUtcTime }) { + // KST 기준 현재 시:분을 추출해, 임계 UTC time-only(1970-01-01) 계산 + const now = new Date(); // UTC + const kstHour = (now.getUTCHours() + 9) % 24; // 현재 KST 시 + const kstMin = now.getUTCMinutes(); + + // KST 00:00에 해당하는 UTC time-only = 1970-01-01T15:00:00Z + const KST_MID_UTC = new Date(Date.UTC(1970, 0, 1, 15, 0, 0, 0)); + // (nowKst - 9h)을 time-only로 표현한 임계치 + const thresholdUtc = new Date(Date.UTC( + 1970, 0, 1, + nowUtcTime.getUTCHours(), nowUtcTime.getUTCMinutes(), nowUtcTime.getUTCSeconds() + )); + + // 기본 where: 오늘(KST) + 아직 요청 상태 + const base = { + status: 0, + serviceDate: { gte: todayStartUtc, lt: tomorrowStartUtc } + }; + + if (kstHour >= 9) { + // 구간 2개: [15:00, 24:00) ∪ [00:00, threshold] + return prisma.helpRequest.updateMany({ + where: { + ...base, + OR: [ + { startTime: { gte: KST_MID_UTC } }, + { startTime: { lte: thresholdUtc } } + ] + }, + data: { status: 4, updatedAt: new Date() } + }); + } else { + // 구간 1개: [15:00, threshold] + return prisma.helpRequest.updateMany({ + where: { + ...base, + startTime: { gte: KST_MID_UTC, lte: thresholdUtc } + }, + data: { status: 4, updatedAt: new Date() } + }); + } + } + + /* async debugCheckRow(helpId, { todayStartUtc, tomorrowStartUtc, nowUtcTime }) { + const row = await prisma.helpRequest.findUnique({ + where: { id: helpId }, + select: { id: true, status: true, serviceDate: true, startTime: true }, + }); + + const inToday = + row.serviceDate >= todayStartUtc && row.serviceDate < tomorrowStartUtc; + const started = row.startTime <= nowUtcTime; + + console.log("[debugCheckRow]", { + id: row.id, + status: row.status, + serviceDate: row.serviceDate.toISOString(), + startTime: row.startTime.toISOString(), + inToday, started, + willUpdate: row.status === 0 && inToday && started, + }); + } */ } export const helpsRepository = new HelpsRepository(); \ No newline at end of file diff --git a/src/jobs/close-expired-helps.job.js b/src/jobs/close-expired-helps.job.js index 2eff0c3..a27c40e 100644 --- a/src/jobs/close-expired-helps.job.js +++ b/src/jobs/close-expired-helps.job.js @@ -1,34 +1,25 @@ import cron from "node-cron"; import { helpsRepository } from "../helps/helps.repository.js"; +import { + kstStartOfTodayAsUtc, + kstStartOfTomorrowAsUtc, + nowUtcTimeOfDayEpoch +} from "../utils/time.js"; -/** - * 오늘의 "KST 자정(00:00)" 시각을 UTC Date 객체로 변환 - * - DB의 serviceDate 비교 기준으로 사용 - * - 예: 2025-09-06 00:00:00 (KST) → 2025-09-05 15:00:00 (UTC) - */ -function kstStartOfTodayAsUtc() { - const now = new Date(); - // 현재 시간을 KST로 변환 - const kstNow = new Date(now.toLocaleString("en-US", { timeZone: "Asia/Seoul" })); - kstNow.setHours(0, 0, 0, 0); // KST 자정으로 맞춤 - // KST → UTC 변환 (9시간 빼기) - return new Date(kstNow.getTime() - 9 * 60 * 60 * 1000); -} - -/** - * 모집종료(4) 상태 업데이트 스케줄러 - * - 서버 시작 시 즉시 1회 실행 - * - 이후 매 30분마다 실행 - */ export function scheduleCloseExpiredHelps() { const run = async () => { - const cutoffUtc = kstStartOfTodayAsUtc(); - const result = await helpsRepository.closeExpiredHelps(cutoffUtc); - console.log( - `[closeExpiredHelps] ${new Date().toISOString()} - updated ${result.count} rows` - ); + const todayStartUtc = kstStartOfTodayAsUtc(); + const tomorrowStartUtc = kstStartOfTomorrowAsUtc(); + const nowUtcTime = nowUtcTimeOfDayEpoch(); + + await helpsRepository.closeExpiredByPastDate(todayStartUtc); + await helpsRepository.closeExpiredByStartTimeToday({ + todayStartUtc, tomorrowStartUtc, nowUtcTime + }); + // await helpsRepository.debugCheckRow(24, { todayStartUtc, tomorrowStartUtc, nowUtcTime }); + }; - run(); // 서버 시작 시 즉시 1회 실행 - cron.schedule("*/30 * * * *", run, { timezone: "Asia/Seoul" }); // 30분마다 실행 + run(); // 서버 시작 즉시 1회 + cron.schedule("*/30 * * * *", run, { timezone: "Asia/Seoul" }); } diff --git a/src/utils/time.js b/src/utils/time.js new file mode 100644 index 0000000..4e21899 --- /dev/null +++ b/src/utils/time.js @@ -0,0 +1,30 @@ +const MS_PER_DAY = 24 * 60 * 60 * 1000; +const KST_OFFSET_MS = 9 * 60 * 60 * 1000; + +/** + * 오늘 KST 00:00을 "UTC Date"로 반환 + * 예) KST 2025-09-07 00:00:00 → UTC 2025-09-06 15:00:00 + */ +export function kstStartOfTodayAsUtc() { + const nowUtcMs = Date.now(); // 현재 UTC epoch(ms) + const kstEpochMs = nowUtcMs + KST_OFFSET_MS; // KST 타임라인으로 이동 + const kstDays = Math.floor(kstEpochMs / MS_PER_DAY);// KST 기준 '오늘'의 일수 + const kstMidnightEpochMs = kstDays * MS_PER_DAY; // KST 자정(epoch, KST 타임라인) + const utcMs = kstMidnightEpochMs - KST_OFFSET_MS; // 다시 UTC로 환산 + return new Date(utcMs); +} + +/** 내일 KST 00:00의 UTC Date */ +export function kstStartOfTomorrowAsUtc() { + const todayUtc = kstStartOfTodayAsUtc(); + return new Date(todayUtc.getTime() + MS_PER_DAY); +} + +/** + * 현재 시간을 UTC time-only(1970-01-01 기준) Date로 반환 + * - DB TIME(@db.Time, UTC)와 안전 비교용 + */ +export function nowUtcTimeOfDayEpoch() { + const now = new Date(); // UTC 기준 시/분/초 사용 + return new Date(Date.UTC(1970, 0, 1, now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds())); +} \ No newline at end of file