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
72 changes: 64 additions & 8 deletions backend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,21 @@
"tags" : [ "Sessions" ],
"summary" : "내 세션 히스토리 (US-15)",
"operationId" : "listSessions",
"parameters" : [ {
"name" : "pageable",
"in" : "query",
"required" : true,
"schema" : {
"$ref" : "#/components/schemas/Pageable"
}
} ],
"responses" : {
"200" : {
"description" : "세션 목록",
"content" : {
"*/*" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/SessionResponse"
}
"$ref" : "#/components/schemas/PageResponseSessionResponse"
}
}
}
Expand All @@ -239,10 +244,7 @@
"content" : {
"*/*" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/SessionResponse"
}
"$ref" : "#/components/schemas/PageResponseSessionResponse"
}
}
}
Expand Down Expand Up @@ -3070,6 +3072,60 @@
}
}
},
"Pageable" : {
"type" : "object",
"properties" : {
"page" : {
"type" : "integer",
"format" : "int32",
"minimum" : 0
},
"size" : {
"type" : "integer",
"format" : "int32",
"minimum" : 1
},
"sort" : {
"type" : "array",
"items" : {
"type" : "string"
}
}
}
},
"PageResponseSessionResponse" : {
"type" : "object",
"properties" : {
"content" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/SessionResponse"
}
},
"page" : {
"type" : "integer",
"format" : "int32"
},
"size" : {
"type" : "integer",
"format" : "int32"
},
"totalElements" : {
"type" : "integer",
"format" : "int64"
},
"totalPages" : {
"type" : "integer",
"format" : "int32"
},
"first" : {
"type" : "boolean"
},
"last" : {
"type" : "boolean"
}
}
},
"FeedbackResponse" : {
"type" : "object",
"properties" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -65,11 +67,10 @@ public SessionResult create(Long userId, SessionCreateCommand command) {
return SessionResult.of(session, linkedIds);
}

public List<SessionResult> list(Long userId) {
public Page<SessionResult> listPaged(Long userId, Pageable pageable) {
loadUser(userId);
return sessionRepository.findByUser_IdAndDeletedFalseOrderByIdDesc(userId).stream()
.map(s -> SessionResult.of(s, contextDocumentIds(s.getId())))
.toList();
return sessionRepository.findByUser_IdAndDeletedFalse(userId, pageable)
.map(s -> SessionResult.of(s, contextDocumentIds(s.getId())));
}

public SessionResult get(Long userId, Long sessionId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

Expand All @@ -15,6 +16,8 @@ public interface InterviewSessionRepository extends JpaRepository<InterviewSessi

List<InterviewSession> findByUser_IdAndDeletedFalseOrderByIdDesc(Long userId);

Page<InterviewSession> findByUser_IdAndDeletedFalse(Long userId, Pageable pageable);

long countByUser_Id(Long userId);

long countByUser_IdAndStatus(Long userId, SessionStatus status);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stackup.stackup.session.presentation;

import com.stackup.stackup.common.response.PageResponse;
import com.stackup.stackup.common.security.UserPrincipal;
import com.stackup.stackup.session.application.SessionService;
import com.stackup.stackup.session.presentation.dto.SessionCreateRequest;
Expand All @@ -11,8 +12,10 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -60,10 +63,14 @@ public SessionResponse create(
@ApiResponse(responseCode = "401", description = "인증 실패")
})
@GetMapping
public List<SessionResponse> list(@AuthenticationPrincipal UserPrincipal principal) {
return sessionService.list(principal.userId()).stream()
.map(SessionResponse::from)
.toList();
public PageResponse<SessionResponse> list(
@AuthenticationPrincipal UserPrincipal principal,
@PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC)
Pageable pageable
) {
return PageResponse.from(
sessionService.listPaged(principal.userId(), pageable).map(SessionResponse::from)
);
}

@Operation(operationId = "getSession", summary = "세션 상세 (US-16)")
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/features/history/api/historyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import type { components } from '@/shared/api/generated'

type S = components['schemas']
export type Session = S['SessionResponse']
export type SessionPage = S['PageResponseSessionResponse']
export type UserStats = S['UserStatsResponse']

export async function listSessions(): Promise<Session[]> {
return (await apiClient.get<Session[]>('/api/sessions')).data
// Spring 이 ?page=&size= 를 바인딩하므로 쿼리 파라미터로 직접 전달.
export async function listSessions(page = 0, size = 20): Promise<SessionPage> {
return (
await apiClient.get<SessionPage>('/api/sessions', { params: { page, size } })
).data
}

export async function getUserStats(): Promise<UserStats> {
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/features/history/model/useHistory.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { useQuery } from '@tanstack/react-query'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { getUserStats, listSessions } from '../api/historyApi'

export const historyKeys = {
sessions: ['history', 'sessions'] as const,
stats: ['history', 'stats'] as const,
}

// 무한 스크롤: last 가 아니면 다음 page 번호를 pageParam 으로.
export function useSessions() {
return useQuery({ queryKey: historyKeys.sessions, queryFn: listSessions })
return useInfiniteQuery({
queryKey: historyKeys.sessions,
queryFn: ({ pageParam }) => listSessions(pageParam),
initialPageParam: 0,
getNextPageParam: (last) => (last.last ? undefined : (last.page ?? 0) + 1),
})
}

export function useUserStats() {
Expand Down
25 changes: 22 additions & 3 deletions frontend/src/features/history/ui/SessionHistoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { useSessions } from '../model/useHistory'
import { SessionCard } from './SessionCard'

export function SessionHistoryList() {
const { data, isLoading, isError, refetch } = useSessions()
const {
data,
isLoading,
isError,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useSessions()

if (isLoading) {
return <p className="py-8 text-center text-body text-fg-muted">불러오는 중…</p>
Expand All @@ -17,17 +25,28 @@ export function SessionHistoryList() {
</div>
)
}
if (!data || data.length === 0) {

const sessions = data?.pages.flatMap((p) => p.content ?? []) ?? []
if (sessions.length === 0) {
return (
<p className="py-8 text-center text-body text-fg-muted">아직 진행한 면접이 없어요.</p>
)
}

return (
<div className="flex flex-col gap-3">
{data.map((s) => (
{sessions.map((s) => (
<SessionCard key={s.id} session={s} />
))}
{hasNextPage && (
<button
className="mt-2 self-center text-caption text-fg-muted underline"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? '불러오는 중…' : '더 보기'}
</button>
)}
</div>
)
}
28 changes: 25 additions & 3 deletions frontend/src/shared/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,26 @@ export interface components {
/** Format: date-time */
timestamp?: string;
};
Pageable: {
/** Format: int32 */
page?: number;
/** Format: int32 */
size?: number;
sort?: string[];
};
PageResponseSessionResponse: {
content?: components["schemas"]["SessionResponse"][];
/** Format: int32 */
page?: number;
/** Format: int32 */
size?: number;
/** Format: int64 */
totalElements?: number;
/** Format: int32 */
totalPages?: number;
first?: boolean;
last?: boolean;
};
FeedbackResponse: {
/** Format: int64 */
id?: number;
Expand Down Expand Up @@ -1312,7 +1332,9 @@ export interface operations {
};
listSessions: {
parameters: {
query?: never;
query: {
pageable: components["schemas"]["Pageable"];
};
header?: never;
path?: never;
cookie?: never;
Expand All @@ -1325,7 +1347,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["SessionResponse"][];
"*/*": components["schemas"]["PageResponseSessionResponse"];
};
};
/** @description 인증 실패 */
Expand All @@ -1334,7 +1356,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["SessionResponse"][];
"*/*": components["schemas"]["PageResponseSessionResponse"];
};
};
};
Expand Down