From 0540db61b8a6e736a6abc39cc5fd1a4f75f33113 Mon Sep 17 00:00:00 2001 From: Sewon Kim Date: Mon, 1 Dec 2025 17:37:03 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EA=B7=BC=EB=A1=9C=EC=9E=90=20?= =?UTF-8?q?=EA=B7=BC=ED=83=9C=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=EA=B0=80=20attendances=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmployeeMyService: attendance_records 대신 attendances 테이블 조회 - DashboardService: 금일 출근/지각 인원을 attendances 테이블에서 조회 - AttendanceRepository: findBySiteIdAndSearchDate 메서드 추가 기존에 attendance_records 테이블을 조회하던 로직이 실제 데이터가 저장되는 attendances 테이블을 조회하도록 변경 Refs: BU-231 --- .../repository/AttendanceRepository.java | 9 + .../employee/service/EmployeeMyService.java | 165 ++++++------------ .../domain/site/service/DashboardService.java | 84 ++------- 3 files changed, 75 insertions(+), 183 deletions(-) diff --git a/src/main/java/com/concrete/buildup/domain/attendance/repository/AttendanceRepository.java b/src/main/java/com/concrete/buildup/domain/attendance/repository/AttendanceRepository.java index 1b67d8c8..47decd1e 100644 --- a/src/main/java/com/concrete/buildup/domain/attendance/repository/AttendanceRepository.java +++ b/src/main/java/com/concrete/buildup/domain/attendance/repository/AttendanceRepository.java @@ -92,6 +92,15 @@ List findByEmployeeIdAndSearchDateBetween( */ List findByEmployeeIdAndSearchDate(Long employeeId, LocalDate searchDate); + /** + * 현장의 특정 날짜 근태 기록 조회 + * + * @param siteId 현장 ID + * @param searchDate 조회 날짜 + * @return 근태 기록 리스트 + */ + List findBySiteIdAndSearchDate(Long siteId, LocalDate searchDate); + /** * 급여 계산용 정상 출근 기록 조회 * diff --git a/src/main/java/com/concrete/buildup/domain/employee/service/EmployeeMyService.java b/src/main/java/com/concrete/buildup/domain/employee/service/EmployeeMyService.java index bf61cdc5..1183f706 100644 --- a/src/main/java/com/concrete/buildup/domain/employee/service/EmployeeMyService.java +++ b/src/main/java/com/concrete/buildup/domain/employee/service/EmployeeMyService.java @@ -1,9 +1,7 @@ package com.concrete.buildup.domain.employee.service; -import com.concrete.buildup.domain.attendance.entity.AttendanceRecord; -import com.concrete.buildup.domain.attendance.enums.AttendanceState; -import com.concrete.buildup.domain.attendance.enums.AttendanceType; -import com.concrete.buildup.domain.attendance.repository.AttendanceRecordRepository; +import com.concrete.buildup.domain.attendance.entity.Attendance; +import com.concrete.buildup.domain.attendance.repository.AttendanceRepository; import com.concrete.buildup.domain.auth.entity.Employee; import com.concrete.buildup.domain.auth.entity.User; import com.concrete.buildup.domain.auth.repository.EmployeeRepository; @@ -59,7 +57,7 @@ public class EmployeeMyService { private final UserRepository userRepository; private final EmployeeRepository employeeRepository; - private final AttendanceRecordRepository attendanceRecordRepository; + private final AttendanceRepository attendanceRepository; private final SiteRepository siteRepository; private final ContractRepository contractRepository; private final PayrollRepository payrollRepository; @@ -72,7 +70,7 @@ public class EmployeeMyService { * 본인 출퇴근 내역 조회 * *

JWT에서 추출한 userId로 본인의 출퇴근 기록을 조회합니다.

- *

날짜별로 그룹핑하여 출근/퇴근 시간을 하나의 레코드로 반환합니다.

+ *

attendances 테이블에서 일별 근태 기록을 조회합니다.

* * @param currentUserId JWT에서 추출한 로그인 ID * @param pageable 페이지네이션 정보 @@ -91,79 +89,44 @@ public MyAttendanceListResponse getMyAttendanceList(String currentUserId, Pageab Long employeeId = employee.getId(); - // 3. 전체 출퇴근 기록 조회 (최근 6개월) - LocalDateTime endTime = LocalDateTime.now(); - LocalDateTime startTime = endTime.minusMonths(6); + // 3. 전체 출퇴근 기록 조회 (최근 6개월) - attendances 테이블 + LocalDate endDate = LocalDate.now(); + LocalDate startDate = endDate.minusMonths(6); - List allRecords = attendanceRecordRepository - .findByEmployeeIdAndTimestampBetween(employeeId, startTime, endTime); + List attendances = attendanceRepository + .findByEmployeeIdAndSearchDateBetween(employeeId, startDate, endDate); - // 4. CONFIRMED 상태만 필터링 - List confirmedRecords = allRecords.stream() - .filter(record -> record.getState() == AttendanceState.CONFIRMED) - .sorted(Comparator.comparing(AttendanceRecord::getTimestamp).reversed()) - .toList(); - - // 5. 날짜별로 그룹핑하여 출근/퇴근 페어 생성 - Map>> recordsByDateAndSite = confirmedRecords.stream() - .collect(Collectors.groupingBy( - record -> record.getTimestamp().toLocalDate(), - LinkedHashMap::new, - Collectors.groupingBy(AttendanceRecord::getSiteId) - )); - - // 6. Site 정보 일괄 조회 (N+1 방지) - Set siteIds = confirmedRecords.stream() - .map(AttendanceRecord::getSiteId) + // 4. Site 정보 일괄 조회 (N+1 방지) + Set siteIds = attendances.stream() + .map(Attendance::getSiteId) .collect(Collectors.toSet()); Map siteMap = siteRepository.findAllById(siteIds).stream() .collect(Collectors.toMap(Site::getId, site -> site)); - // 7. 날짜+현장별 요약 레코드 생성 - List summaries = new ArrayList<>(); - for (Map.Entry>> dateEntry : recordsByDateAndSite.entrySet()) { - LocalDate date = dateEntry.getKey(); - for (Map.Entry> siteEntry : dateEntry.getValue().entrySet()) { - Long siteId = siteEntry.getKey(); - List records = siteEntry.getValue(); - - Site site = siteMap.get(siteId); - String siteName = site != null ? site.getSiteName() : "알 수 없음"; - - // 출근/퇴근 기록 찾기 - AttendanceRecord checkIn = records.stream() - .filter(r -> r.getAttendanceType() == AttendanceType.CHECK_IN) - .findFirst() - .orElse(null); - - AttendanceRecord checkOut = records.stream() - .filter(r -> r.getAttendanceType() == AttendanceType.CHECK_OUT) - .findFirst() - .orElse(null); - - // 출근 기록이 있어야 유효한 데이터 - if (checkIn != null) { - String status = determineStatus(checkIn, checkOut); - Boolean isLate = determineIsLate(checkIn); - - summaries.add(MyAttendanceSummary.builder() - .attendanceId(checkIn.getId()) - .date(date) - .siteId(siteId) + // 5. DTO 변환 (날짜 내림차순 정렬) + List summaries = attendances.stream() + .sorted(Comparator.comparing(Attendance::getSearchDate).reversed()) + .map(attendance -> { + Site site = siteMap.get(attendance.getSiteId()); + String siteName = site != null ? site.getSiteName() : "알 수 없음"; + String status = determineStatus(attendance); + + return MyAttendanceSummary.builder() + .attendanceId(attendance.getId()) + .date(attendance.getSearchDate()) + .siteId(attendance.getSiteId()) .siteName(siteName) - .checkInTime(checkIn.getTimestamp().format(TIME_FORMATTER)) - .checkOutTime(checkOut != null ? checkOut.getTimestamp().format(TIME_FORMATTER) : null) + .checkInTime(attendance.getCheckInTime() != null + ? attendance.getCheckInTime().format(TIME_FORMATTER) : null) + .checkOutTime(attendance.getCheckOutTime() != null + ? attendance.getCheckOutTime().format(TIME_FORMATTER) : null) .status(status) - .isLate(isLate) - .build()); - } - } - } - - // 8. 날짜 내림차순 정렬 - summaries.sort(Comparator.comparing(MyAttendanceSummary::getDate).reversed()); + .isLate(attendance.getIsLate()) + .build(); + }) + .toList(); - // 9. 페이지네이션 적용 + // 6. 페이지네이션 적용 int start = (int) pageable.getOffset(); int end = Math.min(start + pageable.getPageSize(), summaries.size()); @@ -181,33 +144,21 @@ record -> record.getTimestamp().toLocalDate(), /** * 근태 상태 결정 */ - private String determineStatus(AttendanceRecord checkIn, AttendanceRecord checkOut) { - if (checkOut != null) { + private String determineStatus(Attendance attendance) { + if (attendance.getCheckOutTime() != null) { return "COMPLETED"; // 퇴근 완료 } // 출근만 있는 경우 - 당일이면 근무중, 과거면 미퇴근 LocalDate today = LocalDate.now(); - LocalDate checkInDate = checkIn.getTimestamp().toLocalDate(); - if (checkInDate.equals(today)) { + if (attendance.getSearchDate().equals(today)) { return "WORKING"; // 근무중 } else { return "INCOMPLETE"; // 미퇴근 (과거 데이터) } } - /** - * 지각 여부 결정 - *

현재는 단순히 9시 이후 출근을 지각으로 판단합니다.

- *

TODO: 계약서의 출근 시간 기준으로 변경 필요

- */ - private Boolean determineIsLate(AttendanceRecord checkIn) { - LocalTime checkInTime = checkIn.getTimestamp().toLocalTime(); - LocalTime standardTime = LocalTime.of(9, 5); // 9시 5분 기준 (5분 유예) - return checkInTime.isAfter(standardTime); - } - /** * 홈 화면 정보 조회 * @@ -334,46 +285,30 @@ private RecentSalaryInfo getRecentSalary(Long employeeId) { */ private TodayAttendanceInfo getTodayAttendance(Long employeeId) { LocalDate today = LocalDate.now(); - LocalDateTime startOfDay = today.atStartOfDay(); - LocalDateTime endOfDay = today.atTime(23, 59, 59); - - List todayRecords = attendanceRecordRepository - .findByEmployeeIdAndTimestampBetween(employeeId, startOfDay, endOfDay); - // CONFIRMED 상태만 필터링 - List confirmedRecords = todayRecords.stream() - .filter(r -> r.getState() == AttendanceState.CONFIRMED) - .toList(); + // attendances 테이블에서 금일 근태 조회 + List todayAttendances = attendanceRepository + .findByEmployeeIdAndSearchDate(employeeId, today); - if (confirmedRecords.isEmpty()) { + if (todayAttendances.isEmpty()) { return null; } - // 출근/퇴근 기록 찾기 - AttendanceRecord checkIn = confirmedRecords.stream() - .filter(r -> r.getAttendanceType() == AttendanceType.CHECK_IN) - .findFirst() - .orElse(null); - - AttendanceRecord checkOut = confirmedRecords.stream() - .filter(r -> r.getAttendanceType() == AttendanceType.CHECK_OUT) - .findFirst() - .orElse(null); - - if (checkIn == null) { - return null; - } + // 첫 번째 근태 기록 사용 (하루에 여러 현장 근무 가능성 고려) + Attendance attendance = todayAttendances.get(0); - Site site = siteRepository.findById(checkIn.getSiteId()).orElse(null); - String status = checkOut != null ? "COMPLETED" : "WORKING"; + Site site = siteRepository.findById(attendance.getSiteId()).orElse(null); + String status = attendance.getCheckOutTime() != null ? "COMPLETED" : "WORKING"; return TodayAttendanceInfo.builder() - .siteId(checkIn.getSiteId()) + .siteId(attendance.getSiteId()) .siteName(site != null ? site.getSiteName() : "알 수 없음") - .checkInTime(checkIn.getTimestamp().format(TIME_FORMATTER)) - .checkOutTime(checkOut != null ? checkOut.getTimestamp().format(TIME_FORMATTER) : null) + .checkInTime(attendance.getCheckInTime() != null + ? attendance.getCheckInTime().format(TIME_FORMATTER) : null) + .checkOutTime(attendance.getCheckOutTime() != null + ? attendance.getCheckOutTime().format(TIME_FORMATTER) : null) .status(status) - .isLate(determineIsLate(checkIn)) + .isLate(attendance.getIsLate()) .build(); } diff --git a/src/main/java/com/concrete/buildup/domain/site/service/DashboardService.java b/src/main/java/com/concrete/buildup/domain/site/service/DashboardService.java index 34608ec1..a16e0856 100644 --- a/src/main/java/com/concrete/buildup/domain/site/service/DashboardService.java +++ b/src/main/java/com/concrete/buildup/domain/site/service/DashboardService.java @@ -1,9 +1,7 @@ package com.concrete.buildup.domain.site.service; -import com.concrete.buildup.domain.attendance.entity.AttendanceRecord; -import com.concrete.buildup.domain.attendance.enums.AttendanceState; -import com.concrete.buildup.domain.attendance.enums.AttendanceType; -import com.concrete.buildup.domain.attendance.repository.AttendanceRecordRepository; +import com.concrete.buildup.domain.attendance.entity.Attendance; +import com.concrete.buildup.domain.attendance.repository.AttendanceRepository; import com.concrete.buildup.domain.auth.entity.Employee; import com.concrete.buildup.domain.auth.entity.Manager; import com.concrete.buildup.domain.auth.repository.EmployeeRepository; @@ -27,8 +25,6 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; @@ -52,7 +48,7 @@ public class DashboardService { private final SiteRepository siteRepository; private final ContractRepository contractRepository; private final EmployeeRepository employeeRepository; - private final AttendanceRecordRepository attendanceRecordRepository; + private final AttendanceRepository attendanceRepository; private final SafetyEducationLogRepository safetyEducationLogRepository; /** @@ -178,20 +174,20 @@ private DashboardResponse.WorkforceStatus buildWorkforceStatus(Long siteId, Long LocalDate today = LocalDate.now(); - // 금일 출근 인원 (AttendanceRecord에서 CHECK_IN, CONFIRMED 상태) - LocalDateTime startOfDay = today.atStartOfDay(); - LocalDateTime endOfDay = today.atTime(LocalTime.MAX); + // 금일 출근 인원 (attendances 테이블에서 현장별 조회) + List todayAttendances = attendanceRepository + .findBySiteIdAndSearchDate(siteId, today); - List todayAttendanceRecords = attendanceRecordRepository - .findBySiteIdAndTimestampBetween(siteId, startOfDay, endOfDay, Pageable.unpaged()) - .getContent() - .stream() - .filter(record -> record.getAttendanceType() == AttendanceType.CHECK_IN) - .filter(record -> record.getState() == AttendanceState.CONFIRMED) - .collect(Collectors.toList()); + // 중복 제거하여 출근 인원 수 계산 (같은 사람이 여러 기록 있을 수 있음) + int todayAttendance = (int) todayAttendances.stream() + .map(Attendance::getEmployeeId) + .distinct() + .count(); - int todayAttendance = (int) todayAttendanceRecords.stream() - .map(AttendanceRecord::getEmployeeId) + // 금일 지각 인원 계산 + int todayLateCount = (int) todayAttendances.stream() + .filter(a -> Boolean.TRUE.equals(a.getIsLate())) + .map(Attendance::getEmployeeId) .distinct() .count(); @@ -200,7 +196,7 @@ private DashboardResponse.WorkforceStatus buildWorkforceStatus(Long siteId, Long .permanentWorkers(permanentWorkers) .dailyWorkers(dailyWorkers) .todayAttendance(todayAttendance) - .todayLateCount(0) + .todayLateCount(todayLateCount) .build(); } @@ -224,54 +220,6 @@ private boolean isContractActiveOnDate(Contract contract, LocalDate date) { return endDate == null || !date.isAfter(endDate); } - /** - * 금일 지각 인원 계산 - * workStartTime + 5분 이후 출근한 인원 - */ - private int calculateTodayLateCount(List attendanceRecords, List contracts) { - // ContractDetail을 포함한 Contract를 일괄 조회 (N+1 문제 해결) - List contractIds = contracts.stream() - .map(Contract::getId) - .distinct() - .collect(Collectors.toList()); - - if (contractIds.isEmpty()) { - return 0; - } - - // 일괄 조회로 N+1 문제 해결 - Map contractWithDetailsMap = contractRepository.findByIdInWithDetails(contractIds) - .stream() - .collect(Collectors.toMap( - Contract::getEmployeeId, - contract -> contract, - (existing, replacement) -> existing // 중복 시 기존 값 유지 - )); - - int lateCount = 0; - for (AttendanceRecord record : attendanceRecords) { - Contract contract = contractWithDetailsMap.get(record.getEmployeeId()); - if (contract == null || contract.getContractDetail() == null) { - continue; - } - - LocalTime workStartTime = contract.getContractDetail().getWorkStartTime(); - if (workStartTime == null) { - continue; // 출근 시간 정보 없음 - } - - // 지각 기준: workStartTime + 5분 - LocalTime lateThreshold = workStartTime.plusMinutes(5); - LocalTime checkInTime = record.getTimestamp().toLocalTime(); - - if (checkInTime.isAfter(lateThreshold)) { - lateCount++; - } - } - - return lateCount; - } - /** * 안전 현황 구성 (임의 값) */ From 826b0f461cf7a44c8d6e72f1151bac450c0f195b Mon Sep 17 00:00:00 2001 From: Sewon Kim Date: Mon, 1 Dec 2025 17:39:34 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=93=A0=20API=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=EC=97=90=20/v1=20?= =?UTF-8?q?prefix=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmployeeController: /sites/{siteId}/employees → /v1/sites/{siteId}/employees - DocumentController: /documents → /v1/documents Refs: BU-231 --- DEMO-SCENARIO.md | 1069 +++++++++++++++++ .../controller/DocumentController.java | 2 +- .../controller/EmployeeController.java | 2 +- 3 files changed, 1071 insertions(+), 2 deletions(-) create mode 100644 DEMO-SCENARIO.md diff --git a/DEMO-SCENARIO.md b/DEMO-SCENARIO.md new file mode 100644 index 00000000..744cce12 --- /dev/null +++ b/DEMO-SCENARIO.md @@ -0,0 +1,1069 @@ +# Build-Up Platform 시연 시나리오 + +> 건설 현장 인력 관리 플랫폼 최종 발표 시연 대본 + +--- + +## 목차 + +1. [시연 개요](#시연-개요) +2. [테스트 데이터](#테스트-데이터) +3. [시나리오 1: 초기 설정 (신규 현장 개설)](#시나리오-1-초기-설정) +4. [시나리오 2: 일일 현장 운영](#시나리오-2-일일-현장-운영) +5. [시연 플로우 요약](#시연-플로우-요약) + +--- + +## 시연 개요 + +### 서비스 소개 + +**Build-Up Platform**은 건설 현장의 인력 관리를 디지털화하는 통합 플랫폼입니다. + +- **전자 근로계약**: PDF 생성 및 전자서명 (관리자 → 근로자 2단계) +- **얼굴 인식 출퇴근**: Face Recognition 기반 비접촉 근태 관리 +- **작업일보/안전교육일지**: 실시간 문서 생성 및 PDF 아카이빙 +- **급여 자동 계산**: 근태 기반 급여 산출 및 명세서 제공 + +### 사용자 역할 + +| 역할 | 설명 | 주요 기능 | +|------|------|----------| +| **CORPORATION** | 기업 관리자 (본사) | 현장 등록, 전체 현황 모니터링, 급여 관리 | +| **MANAGER** | 현장 관리자 (소장) | 계약 생성, 출퇴근 관리, 작업일보/안전교육 작성 | +| **EMPLOYEE** | 근로자 | 계약 서명, 얼굴 등록, 본인 정보 조회 | + +### 시연 시간 + +- **총 시연 시간**: 약 12-15분 +- **시나리오 1 (초기 설정)**: 약 6분 +- **시나리오 2 (일일 운영)**: 약 6분 + +--- + +## 테스트 데이터 + +### 기업 정보 + +| 항목 | 값 | +|------|-----| +| 기업명 | 빌드업건설(주) | +| 대표자 | 김건설 | +| 사업자번호 | 123-45-67890 | +| 주소 | 서울시 강남구 테헤란로 123 | + +### 현장 정보 + +| 항목 | 값 | +|------|-----| +| 현장명 | 세종대학교 AI센터 신축공사 | +| 주소 | 서울시 광진구 능동로 209 | +| 발주처 | 세종대학교 산학협력단 | +| 공사 기간 | 2025-01-01 ~ 2025-12-31 | + +### 계정 정보 + +#### 기업 관리자 +| 항목 | 값 | +|------|-----| +| 아이디 | buildup_corp | +| 비밀번호 | Test1234! | +| 이름 | 김건설 | + +#### 현장 관리자 +| 항목 | 값 | +|------|-----| +| 아이디 | site_manager01 | +| 비밀번호 | Manager123! | +| 이름 | 박현장 | +| 연락처 | 010-1234-5678 | + +#### 근로자 (3명) + +| 구분 | 아이디 | 이름 | 고용형태 | 직종 | +|------|--------|------|----------|------| +| 근로자 A | worker_kim | 김철수 | 상용직 | 철근공 | +| 근로자 B | worker_lee | 이영희 | 상용직 | 용접공 | +| 근로자 C | worker_park | 박민수 | 일용직 | 보통인부 | + +--- + +## 시나리오 1: 초기 설정 + +> 새로운 건설 현장을 개설하고, 관리자와 근로자를 등록하여 계약을 체결하는 과정입니다. + +--- + +### 1-1. 서비스 접속 및 로그인 화면 + +**[화면: 로그인 페이지]** + +``` +(나레이션) +"Build-Up Platform 시연을 시작하겠습니다. + +Build-Up은 건설 현장의 인력 관리를 디지털화하는 통합 플랫폼입니다. +전자 근로계약, 얼굴 인식 출퇴근, 작업일보 및 안전교육일지 관리, +그리고 급여 자동 계산까지 건설 현장 운영에 필요한 모든 기능을 제공합니다. + +먼저 기업 관리자로 로그인하여 새로운 현장을 등록해 보겠습니다." +``` + +**[액션]** +1. 아이디 입력: `buildup_corp` +2. 비밀번호 입력: `Test1234!` +3. 로그인 버튼 클릭 + +--- + +### 1-2. 현장 등록 (기업 관리자) + +**[화면: 현장 관리 > 현장 등록]** + +``` +(나레이션) +"로그인 후 기업 관리자 대시보드가 표시됩니다. +좌측 사이드바에서 '현장 관리' 메뉴를 통해 새로운 현장을 등록할 수 있습니다. + +현장 등록 시에는 현장명, 주소, 발주처, 공사 기간을 입력합니다." +``` + +**[액션]** +1. 좌측 메뉴에서 "현장 관리" 클릭 +2. "현장 등록" 버튼 클릭 +3. 현장 정보 입력: + - 현장명: `세종대학교 AI센터 신축공사` + - 주소: `서울시 광진구 능동로 209` + - 발주처: `세종대학교 산학협력단` + - 시작일: `2025-01-01` + - 종료일: `2025-12-31` +4. "등록" 버튼 클릭 + +**[화면: 시크릿키 발급 완료]** + +``` +(나레이션) +"현장이 등록되면 두 개의 시크릿키가 자동으로 생성됩니다. + +'관리자 시크릿키'는 현장 관리자가 회원가입할 때 사용하고, +'근로자 시크릿키'는 해당 현장에서 일할 근로자들이 회원가입할 때 사용합니다. + +이 시크릿키를 통해 사용자는 자동으로 해당 현장에 배정됩니다. +기업 관리자는 이 시크릿키를 현장 관리자와 근로자에게 전달합니다." +``` + +**[표시되는 정보]** +- 관리자 시크릿키: `MGR-SEJONG-2025-XXXX` +- 근로자 시크릿키: `EMP-SEJONG-2025-XXXX` + +--- + +### 1-3. 현장 관리자 회원가입 + +**[화면: 로그아웃 → 회원가입 페이지]** + +``` +(나레이션) +"이제 현장 관리자 계정을 등록해 보겠습니다. +현장 관리자는 기업 관리자로부터 전달받은 '관리자 시크릿키'를 사용하여 회원가입합니다." +``` + +**[액션]** +1. "현장 관리자로 가입" 선택 +2. 정보 입력: + - 아이디: `site_manager01` + - 비밀번호: `Manager123!` + - 비밀번호 확인: `Manager123!` + - 이름: `박현장` + - 시크릿키: `MGR-SEJONG-2025-XXXX` (복사하여 붙여넣기) +3. "가입하기" 버튼 클릭 + +``` +(나레이션) +"시크릿키가 유효하면 회원가입이 완료되고, +자동으로 '세종대학교 AI센터 신축공사' 현장에 배정됩니다. + +이제 현장 관리자로 로그인해 보겠습니다." +``` + +--- + +### 1-4. 근로자 회원가입 + +**[화면: 회원가입 페이지 - 근로자]** + +``` +(나레이션) +"다음으로 근로자 회원가입을 진행합니다. +근로자 회원가입은 개인정보 보호를 위해 2단계로 진행됩니다. + +1단계에서는 기본 정보와 시크릿키를 입력하고, +2단계에서는 주민등록번호, 연락처, 주소 등 민감한 개인정보를 입력합니다. +주민등록번호는 AES-256-GCM 암호화로 안전하게 저장됩니다." +``` + +**[액션 - 1단계]** +1. "근로자로 가입" 선택 +2. 정보 입력: + - 아이디: `worker_kim` + - 비밀번호: `Worker123!` + - 이름: `김철수` + - 시크릿키: `EMP-SEJONG-2025-XXXX` +3. "다음" 버튼 클릭 + +**[액션 - 2단계]** +``` +(나레이션) +"2단계에서는 근로자의 상세 정보를 입력합니다. +입력된 주민등록번호는 AES-256-GCM 방식으로 암호화되어 저장되며, +조회 시에도 마스킹 처리되어 보안을 유지합니다." +``` + +1. 주민등록번호 입력: `850101-1******` +2. 연락처: `010-1111-2222` +3. 주소: `서울시 성동구 왕십리로 123` +4. 비상연락처: `010-3333-4444` +5. "가입 완료" 버튼 클릭 + +``` +(나레이션) +"같은 방식으로 근로자 이영희, 박민수도 회원가입을 완료합니다. +총 3명의 근로자가 현장에 등록되었습니다." +``` + +--- + +### 1-5. 근로계약서 생성 및 전자서명 + +**[화면: 현장 관리자 로그인 → 계약 관리]** + +``` +(나레이션) +"이제 현장 관리자로 로그인하여 근로계약서를 생성하겠습니다. +Build-Up의 전자계약 시스템은 3단계 서명 프로세스를 지원합니다. + +첫째, 계약서 초안 PDF를 생성하고, +둘째, 현장 관리자가 서명하면 v2 PDF가 생성되며, +셋째, 근로자가 서명하면 최종 v3 PDF가 생성됩니다. + +각 단계마다 SHA-256 해시값이 기록되어 문서의 무결성을 보장합니다." +``` + +**[액션 - 계약 생성]** +1. "계약 관리" 메뉴 클릭 +2. "새 계약 작성" 버튼 클릭 +3. 고용 형태 선택: "상용직" +4. 근로자 선택: "김철수" +5. 계약 정보 입력: + - 계약 시작일: `2025-01-15` + - 계약 종료일: `2025-12-31` + - 직종: `철근공` + - 월 급여: `3,500,000원` + - 출근 시간: `08:00` + - 퇴근 시간: `17:00` + - 휴게 시간: `12:00 ~ 13:00` +6. "계약서 생성" 버튼 클릭 + +**[화면: 계약서 PDF 미리보기]** + +``` +(나레이션) +"계약 정보를 입력하면 Thymeleaf 템플릿과 Flying Saucer 라이브러리를 사용하여 +표준 양식의 근로계약서 PDF가 자동으로 생성됩니다. + +생성된 PDF는 S3에 저장되며, 이것이 v1 초안 PDF입니다. +이제 현장 관리자가 서명하겠습니다." +``` + +**[액션 - 관리자 서명]** +1. PDF 미리보기 확인 +2. "서명하기" 버튼 클릭 +3. 서명 패드에 서명 입력 +4. "서명 완료" 버튼 클릭 + +``` +(나레이션) +"관리자 서명이 완료되면 v2 PDF가 생성됩니다. +서명 이미지는 지정된 좌표에 정확히 스탬핑되며, +서명 시점, IP 주소, 디바이스 정보가 함께 기록됩니다. + +이제 근로자가 서명할 차례입니다. +계약 상태가 '근로자 서명 대기'로 변경된 것을 확인할 수 있습니다." +``` + +**[화면: 근로자 로그인 → 홈]** + +``` +(나레이션) +"근로자 김철수로 로그인하면, 홈 화면에서 미서명 계약서 알림을 확인할 수 있습니다. +'서명 대기 중인 계약서가 있습니다'라는 안내가 표시됩니다." +``` + +**[액션 - 근로자 서명]** +1. 홈 화면의 "미결 계약" 카드 클릭 +2. 계약서 내용 확인 +3. "서명하기" 버튼 클릭 +4. 서명 패드에 서명 입력 +5. "서명 완료" 버튼 클릭 + +``` +(나레이션) +"근로자 서명이 완료되면 최종 v3 PDF가 생성됩니다. +계약 상태가 '서명 완료'로 변경되고, 전체 PDF의 SHA-256 해시값이 저장됩니다. + +이 해시값을 통해 언제든지 계약서의 위변조 여부를 검증할 수 있습니다. +동일한 방식으로 이영희, 박민수의 계약도 진행합니다." +``` + +--- + +### 1-6. 얼굴 등록 (출퇴근용) + +**[화면: 근로자 홈 → 얼굴 등록]** + +``` +(나레이션) +"Build-Up은 얼굴 인식 기반 출퇴근 시스템을 제공합니다. +근로자는 최초 1회 자신의 얼굴을 등록해야 합니다. + +등록된 얼굴 이미지는 Face-api.js 라이브러리로 임베딩 벡터가 추출되어 저장되며, +출퇴근 시 코사인 유사도로 본인 확인을 수행합니다." +``` + +**[액션]** +1. "내 정보" 메뉴 클릭 +2. "얼굴 등록" 버튼 클릭 +3. 카메라로 정면 얼굴 촬영 +4. "등록" 버튼 클릭 + +``` +(나레이션) +"얼굴이 성공적으로 등록되었습니다. +이제 현장에 비치된 공용 태블릿에서 얼굴 인식으로 출퇴근할 수 있습니다. + +이것으로 초기 설정이 완료되었습니다. +현장 등록, 관리자/근로자 회원가입, 근로계약 체결, 얼굴 등록까지 +건설 현장 운영을 위한 기본 준비가 끝났습니다." +``` + +--- + +## 시나리오 2: 일일 현장 운영 + +> 시나리오 1에서 설정된 데이터를 기반으로, 일일 현장 운영 과정을 시연합니다. +> 이미 계약이 체결되고 얼굴 등록이 완료된 상태입니다. + +--- + +### 2-1. 출퇴근 기록 (공용 태블릿) + +**[화면: 공용 태블릿 - 출퇴근 화면]** + +``` +(나레이션) +"이제 일일 현장 운영 과정을 살펴보겠습니다. +먼저 현장에 비치된 공용 태블릿에서 근로자의 출퇴근을 기록합니다. + +Build-Up의 얼굴 인식 시스템은 코사인 유사도 0.35 이상을 기준으로 +등록된 근로자 본인임을 확인합니다. +이 임계값은 오인식률과 미인식률의 균형을 고려하여 설정되었습니다." +``` + +**[액션 - 김철수 출근]** +1. 태블릿에서 "출퇴근 기록" 선택 +2. 전화번호 입력: `010-1111-2222` +3. 카메라로 얼굴 촬영 +4. 얼굴 인식 중... (로딩) +5. "출근이 기록되었습니다!" 메시지 표시 + - 출근 시간: `07:55` + - 유사도: `0.92` + - 상태: `정상 출근` + +``` +(나레이션) +"김철수 님의 출근이 기록되었습니다. +유사도 0.92로 등록된 얼굴과 높은 일치도를 보입니다. + +출근 시간이 08:00이고 현재 07:55이므로 정상 출근으로 처리됩니다. +만약 08:05 이후에 출근하면 지각으로 기록됩니다. +지각 기준은 계약서에 명시된 출근 시간 +5분입니다." +``` + +**[액션 - 이영희 출근 (지각)]** +``` +(나레이션) +"이번에는 이영희 님이 08:15에 출근하는 경우를 보겠습니다." +``` + +1. 전화번호 입력: `010-2222-3333` +2. 얼굴 촬영 +3. "출근이 기록되었습니다!" 메시지 + - 출근 시간: `08:15` + - 유사도: `0.89` + - 상태: `지각` + +``` +(나레이션) +"이영희 님은 08:15에 출근하여 지각으로 기록되었습니다. +지각 여부는 급여 계산 시 자동으로 반영됩니다." +``` + +**[액션 - 퇴근 기록]** +``` +(나레이션) +"퇴근도 동일한 방식으로 기록됩니다. +같은 날 두 번째 얼굴 인식은 자동으로 퇴근으로 처리됩니다." +``` + +--- + +### 2-2. 안전교육 실시 및 서명 + +**[화면: 현장 관리자 → 안전교육 관리]** + +``` +(나레이션) +"건설 현장에서는 매일 또는 정기적으로 안전교육을 실시해야 합니다. +Build-Up은 안전교육일지 작성과 참석자 전자서명 기능을 제공합니다." +``` + +**[액션 - 안전교육 생성]** +1. "안전교육 관리" 메뉴 클릭 +2. "새 안전교육 등록" 버튼 클릭 +3. 교육 정보 입력: + - 교육 일자: `2025-01-20` + - 교육 구분: `정기교육` + - 교육 과목: `추락재해 예방교육` + - 교육 내용: `고소작업 시 안전대 착용 및 안전난간 설치 요령` + - 교육 실시자: `박현장` + - 교육 장소: `현장 사무실` +4. 참석자 선택: 김철수, 이영희, 박민수 (전체 선택) +5. "안전교육 등록" 버튼 클릭 + +``` +(나레이션) +"안전교육이 등록되면 PDF 초안이 자동으로 생성됩니다. +근로계약서와 마찬가지로 관리자 서명 후 참석자 서명을 받아야 완료됩니다." +``` + +**[액션 - 관리자 서명]** +1. 생성된 안전교육일지 클릭 +2. PDF 미리보기 확인 +3. "서명하기" 버튼 클릭 +4. 서명 패드에 서명 +5. "서명 완료" 클릭 + +``` +(나레이션) +"관리자 서명이 완료되었습니다. +이제 참석자들이 각자 서명해야 합니다. +참석자 서명 현황을 실시간으로 확인할 수 있습니다." +``` + +**[화면: 참석자 서명 현황]** +- 김철수: ❌ 미서명 +- 이영희: ❌ 미서명 +- 박민수: ❌ 미서명 + +**[액션 - 참석자 서명 (김철수)]** +``` +(나레이션) +"참석자들은 자신의 계정으로 로그인하여 안전교육 서명을 진행합니다. +근로자 홈 화면에서 미서명 안전교육 알림을 확인할 수 있습니다." +``` + +1. 근로자 김철수로 로그인 +2. 홈 화면에서 "안전교육 서명 대기" 알림 클릭 +3. 교육 내용 확인 +4. "서명하기" 버튼 클릭 +5. 서명 완료 + +``` +(나레이션) +"같은 방식으로 이영희, 박민수도 서명을 완료합니다. +모든 참석자가 서명하면 최종 PDF가 생성되고 안전교육이 완료 처리됩니다." +``` + +**[화면: 참석자 서명 현황 (완료)]** +- 김철수: ✅ 서명완료 +- 이영희: ✅ 서명완료 +- 박민수: ✅ 서명완료 +- **상태: 교육 완료** + +--- + +### 2-3. 작업일보 작성 + +**[화면: 현장 관리자 → 작업일보 관리]** + +``` +(나레이션) +"하루의 작업이 끝나면 현장 관리자는 작업일보를 작성합니다. +작업일보에는 당일 투입된 인원, 진행된 공정, 사용된 자재 등을 기록합니다." +``` + +**[액션]** +1. "작업일보 관리" 메뉴 클릭 +2. "작업일보 작성" 버튼 클릭 +3. 작업일보 정보 입력: + - 작성 일자: `2025-01-20` + - 날씨: `맑음` + - 공정명: `기초공사 - 철근 배근` + - 금일 작업 내용: + ``` + 1. 기초 슬래브 하부 철근 배근 작업 + 2. 스페이서 설치 및 피복두께 확인 + 3. 배근 상태 자체 검측 + ``` + - 투입 인원: + - 상용직: 2명 + - 일용직: 1명 + - 사용 자재: + - 철근 D25: 500kg + - 철근 D19: 300kg + - 스페이서: 200개 +4. "저장 및 PDF 생성" 버튼 클릭 + +``` +(나레이션) +"작업일보가 저장되면 즉시 PDF가 생성됩니다. +생성된 PDF는 S3에 날짜별로 정리되어 저장되며, +언제든지 조회하고 다운로드할 수 있습니다. + +같은 날짜에 여러 개의 작업일보를 작성할 수도 있으며, +이 경우 순번(sequence)으로 구분됩니다." +``` + +**[화면: 작업일보 목록]** +- 2025-01-20 작업일보 #1 - `WR-2025-01-20-1.pdf` +- 상태: 작성 완료 ✅ + +--- + +### 2-4. 현장 대시보드 확인 (기업 관리자) + +**[화면: 기업 관리자 로그인 → 메인 화면]** + +``` +(나레이션) +"이제 기업 관리자가 전체 현장 현황을 확인해 보겠습니다. +기업 관리자 계정으로 로그인하면 메인 대시보드 화면이 표시됩니다." +``` + +**[액션]** +1. 아이디: `buildup_corp` 입력 +2. 비밀번호: `Test1234!` 입력 +3. "로그인" 버튼 클릭 + +--- + +**[화면: 좌측 사이드바 - 현장 목록]** + +``` +(나레이션) +"로그인 후 화면 좌측에는 사이드바가 표시됩니다. +사이드바에는 기업 관리자가 담당하는 모든 현장 목록이 나타납니다. + +빌드업건설(주)은 현재 3개의 현장을 운영하고 있으며, +각 현장명을 클릭하면 해당 현장의 상세 대시보드로 이동합니다. + +'세종대학교 AI센터 신축공사' 현장을 선택해 보겠습니다." +``` + +**[사이드바 구조]** +``` +┌─────────────────────────┐ +│ 빌드업건설(주) │ +│ ───────────────────── │ +│ 📍 현장 목록 │ +│ ├─ 세종대학교 AI센터 ◀ │ +│ ├─ 강남 오피스텔 신축 │ +│ └─ 판교 물류센터 증축 │ +│ ───────────────────── │ +│ ➕ 현장 등록 │ +└─────────────────────────┘ +``` + +**[액션]** +1. 좌측 사이드바에서 "세종대학교 AI센터" 클릭 + +--- + +**[화면: 현장 기본 정보 헤더]** + +``` +(나레이션) +"현장을 선택하면 상단에 현장의 기본 정보가 표시됩니다. +현장명, 주소, 발주처, 공사 기간과 함께 현재 공사 진행률을 확인할 수 있습니다. + +세종대학교 AI센터 신축공사는 2025년 1월부터 12월까지 진행 예정이며, +현재 약 8%의 공정이 완료된 상태입니다." +``` + +**[현장 헤더 정보]** +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 🏗️ 세종대학교 AI센터 신축공사 │ +│ ───────────────────────────────────────────────────────────────── │ +│ 📍 주소: 서울시 광진구 능동로 209 │ +│ 🏢 발주처: 세종대학교 산학협력단 │ +│ 📅 공사 기간: 2025-01-01 ~ 2025-12-31 │ +│ ───────────────────────────────────────────────────────────────── │ +│ 공정 진행률: [████░░░░░░░░░░░░░░░░] 8% │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +**[화면: 대시보드 - 3대 핵심 지표]** + +``` +(나레이션) +"헤더 아래에는 현장 운영의 핵심이 되는 세 가지 지표가 카드 형태로 표시됩니다. + +첫 번째는 '인원 현황' 카드입니다. +이 현장에 등록된 총 근로자 수, 상용직과 일용직의 비율, +그리고 오늘 출근한 인원과 지각자 수를 실시간으로 보여줍니다. +현재 총 3명의 근로자 중 전원이 출근했고, 이영희 님이 지각한 것을 확인할 수 있습니다. + +두 번째는 '안전 현황' 카드입니다. +건설 현장에서 가장 중요한 안전교육 이수 현황을 보여줍니다. +전체 근로자 중 안전교육을 이수한 비율과 미이수 인원, +그리고 가장 최근에 실시한 안전교육 정보가 표시됩니다. +현재 안전율 100%로 모든 근로자가 교육을 이수했습니다. + +세 번째는 '노무 현황' 카드입니다. +근로계약 체결 현황과 급여 지급 상태를 보여줍니다. +미결 계약이 있다면 빨간색으로 표시되어 빠른 조치가 필요함을 알려줍니다. +현재 모든 계약이 완료되어 미결 계약 0건, 급여도 정상 처리 중입니다." +``` + +**[대시보드 3대 지표 카드]** +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ 👷 인원 현황 │ │ 🦺 안전 현황 │ │ 📋 노무 현황 │ │ +│ │ ───────────────── │ │ ───────────────── │ │ ───────────────── │ │ +│ │ 총 인원 3명 │ │ 안전율 100% │ │ 미결 계약 0건 │ │ +│ │ ───────────────── │ │ ───────────────── │ │ ───────────────── │ │ +│ │ 상용직 2명 │ │ 미이수 0명 │ │ 서명대기 0건 │ │ +│ │ 일용직 1명 │ │ ───────────────── │ │ ───────────────── │ │ +│ │ ───────────────── │ │ 최근 교육: │ │ 금월 미지급 0건 │ │ +│ │ 금일 출근 3명 │ │ 추락재해 예방교육 │ │ │ │ +│ │ 금일 지각 1명 │ │ (2025-01-20 완료) │ │ │ │ +│ │ ⚠️ 이영희 │ │ │ │ │ │ +│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +**[화면: 대시보드 - 하단 탭 메뉴]** + +``` +(나레이션) +"대시보드 하단에는 네 가지 탭 메뉴가 있습니다. + +'문서 관리' 탭에서는 날짜별 안전교육일지와 작업일보를 통합 조회할 수 있습니다. +'계약 관리' 탭에서는 현장의 모든 근로계약서를 상태별로 관리합니다. +'근태 관리' 탭에서는 근로자들의 출퇴근 기록을 조회합니다. +'급여 관리' 탭에서는 상용직과 일용직의 급여 현황을 확인합니다. + +각 탭을 클릭하면 상세 정보를 확인할 수 있습니다. +먼저 문서 관리 탭을 살펴보겠습니다." +``` + +**[탭 메뉴 구조]** +``` +┌──────────────────────────────────────────────────────────────────┐ +│ [ 📄 문서 관리 ] [ 📝 계약 관리 ] [ ⏰ 근태 관리 ] [ 💰 급여 관리 ] │ +│ ▲ │ +│ 현재 선택 │ +└──────────────────────────────────────────────────────────────────┘ +``` + +--- + +**[화면: 대시보드 기능 요약]** + +``` +(나레이션) +"이처럼 기업 관리자는 대시보드 한 화면에서 +인력, 안전, 노무의 세 가지 핵심 지표를 실시간으로 모니터링하고, +하단 탭을 통해 상세 정보에 빠르게 접근할 수 있습니다. + +여러 현장을 운영하는 경우에도 좌측 사이드바에서 +현장을 전환하며 각 현장의 상황을 즉시 파악할 수 있습니다. + +이제 각 탭의 상세 기능을 살펴보겠습니다." +``` + +--- + +### 2-5. 안전/작업 문서 조회 + +**[화면: 대시보드 → 안전/작업 문서 탭]** + +``` +(나레이션) +"대시보드 하단의 '안전/작업 문서' 탭에서는 +날짜별로 안전교육일지와 작업일보를 통합하여 조회할 수 있습니다. + +연도와 월을 선택하여 필터링할 수 있으며, +각 문서를 클릭하면 상세 내용과 PDF를 확인할 수 있습니다." +``` + +**[화면: 문서 목록]** +| 날짜 | 안전교육일지 | 작업일보 | +|------|------------|---------| +| 2025-01-20 | ✅ 추락재해 예방교육 (완료) | ✅ #1 작성완료 | +| 2025-01-19 | ✅ 정리정돈 교육 (완료) | ✅ #1 작성완료 | +| 2025-01-18 | - | ✅ #1 작성완료 | + +--- + +### 2-6. 근태 현황 조회 + +**[화면: 근태 관리 탭]** + +``` +(나레이션) +"'근태 관리' 탭에서는 현장 전체의 출퇴근 기록을 조회합니다. +월별, 일별로 조회할 수 있으며 상용직과 일용직을 구분하여 확인할 수 있습니다. + +각 근로자의 출근 시간, 퇴근 시간, 지각 여부, 근무 시간이 표시됩니다." +``` + +**[화면: 2025년 1월 20일 근태 현황]** +| 이름 | 고용형태 | 출근 | 퇴근 | 지각 | 근무시간 | +|------|---------|------|------|------|---------| +| 김철수 | 상용직 | 07:55 | 17:05 | - | 8시간 | +| 이영희 | 상용직 | 08:15 | 17:10 | ⚠️ 지각 | 7시간 55분 | +| 박민수 | 일용직 | 08:00 | 17:00 | - | 8시간 | + +--- + +### 2-7. 급여 관리 (월말) + +**[화면: 급여 관리 탭]** + +``` +(나레이션) +"월말이 되면 급여 현황을 확인할 수 있습니다. +Build-Up은 근태 기록을 기반으로 급여를 자동 계산합니다. + +상용직은 월급 기준으로, 일용직은 일급 기준으로 계산되며, +지각, 연장근로, 야간근로, 휴일근로 수당이 자동으로 반영됩니다." +``` + +**[액션]** +1. "급여 관리" 탭 클릭 +2. 고용 형태: "상용직" 선택 +3. 연도/월: "2025년 1월" 선택 + +**[화면: 2025년 1월 상용직 급여 현황]** +| 이름 | 기본급 | 연장수당 | 지각공제 | 총 지급액 | 상태 | +|------|--------|---------|---------|----------|------| +| 김철수 | 3,500,000 | 150,000 | 0 | 3,650,000 | 미지급 | +| 이영희 | 3,500,000 | 100,000 | -50,000 | 3,550,000 | 미지급 | + +``` +(나레이션) +"김철수 님은 연장근로 수당 15만원이 추가되어 총 365만원, +이영희 님은 연장근로 수당 10만원이 추가되고 지각 2회로 5만원이 공제되어 +총 355만원이 산출되었습니다. + +급여명세서 상세 보기를 클릭하면 근무일지와 함께 +세부 급여 항목을 확인할 수 있습니다." +``` + +**[액션 - 급여명세서 상세]** +1. "김철수" 행 클릭 +2. 급여명세서 상세 페이지 표시 + +**[화면: 김철수 급여명세서]** +``` +┌────────────────────────────────────────────────┐ +│ 2025년 1월 급여명세서 │ +│ 근로자: 김철수 | 직종: 철근공 | 고용형태: 상용직 │ +├────────────────────────────────────────────────┤ +│ [지급 항목] │ +│ 기본급 3,500,000원 │ +│ 연장근로수당 150,000원 (10시간 × 15,000원) │ +│ 야간근로수당 0원 │ +│ 휴일근로수당 0원 │ +│ ────────────────────────────── │ +│ 지급 합계 3,650,000원 │ +├────────────────────────────────────────────────┤ +│ [공제 항목] │ +│ 지각 공제 0원 (0회) │ +│ ────────────────────────────── │ +│ 공제 합계 0원 │ +├────────────────────────────────────────────────┤ +│ 실 지급액 3,650,000원 │ +├────────────────────────────────────────────────┤ +│ [근무일지] │ +│ 총 근무일: 20일 | 지각: 0회 | 연장: 10시간 │ +│ 01/02 08:00-17:00 (8h) │ +│ 01/03 08:00-18:00 (9h) +1h 연장 │ +│ 01/04 07:55-17:05 (8h) │ +│ ... │ +└────────────────────────────────────────────────┘ +``` + +--- + +### 2-8. 근로자 본인 조회 + +**[화면: 근로자 로그인 → 홈]** + +``` +(나레이션) +"마지막으로 근로자가 본인의 정보를 조회하는 화면입니다. +근로자 홈 화면에서는 세 가지 핵심 정보를 확인할 수 있습니다. + +첫째, 미결 전자계약 - 서명이 필요한 계약서나 안전교육일지 +둘째, 최근 급여 - 가장 최근 지급된 급여 정보 +셋째, 금일 근태 - 오늘의 출퇴근 기록" +``` + +**[화면: 근로자 김철수 홈]** +``` +┌────────────────────────────────────────────────┐ +│ 안녕하세요, 김철수 님! │ +│ 세종대학교 AI센터 신축공사 │ +├────────────────────────────────────────────────┤ +│ 📋 미결 전자계약 │ +│ └─ 없음 ✅ │ +├────────────────────────────────────────────────┤ +│ 💰 최근 급여 │ +│ └─ 2025년 1월: 3,650,000원 (미지급) │ +├────────────────────────────────────────────────┤ +│ ⏰ 금일 근태 │ +│ └─ 출근: 07:55 | 퇴근: 17:05 | 정상 │ +└────────────────────────────────────────────────┘ +``` + +**[액션 - 출퇴근 내역 조회]** +``` +(나레이션) +"하단 메뉴의 '출퇴근 내역'을 클릭하면 +최근 6개월간의 출퇴근 기록을 확인할 수 있습니다." +``` + +**[액션 - 급여 내역 조회]** +``` +(나레이션) +"'급여 내역'에서는 본인의 모든 급여명세서를 조회할 수 있습니다. +각 명세서를 클릭하면 상세 항목과 근무일지를 확인할 수 있습니다." +``` + +--- + +### 2-9. 마무리 + +**[화면: 기업 관리자 대시보드]** + +``` +(나레이션) +"이상으로 Build-Up Platform의 시연을 마치겠습니다. + +Build-Up은 건설 현장의 복잡한 인력 관리 업무를 디지털화하여 +기업 관리자, 현장 관리자, 근로자 모두에게 편의를 제공합니다. + +전자 근로계약으로 종이 문서를 없애고, +얼굴 인식으로 정확한 출퇴근 기록을 남기며, +자동화된 급여 계산으로 노무 관리의 효율을 높입니다. + +모든 문서는 PDF로 아카이빙되어 법적 증빙으로 활용할 수 있으며, +SHA-256 해시 검증을 통해 문서 무결성을 보장합니다. + +시청해 주셔서 감사합니다." +``` + +--- + +## 시연 플로우 요약 + +### 시나리오 1: 초기 설정 (약 6분) + +``` +기업 관리자 로그인 + │ + ▼ +현장 등록 (시크릿키 발급) + │ + ├──→ 관리자 시크릿키 → 현장 관리자 회원가입 + │ + └──→ 근로자 시크릿키 → 근로자 회원가입 (2단계) + │ + ▼ + 얼굴 등록 + │ + ▼ +현장 관리자 로그인 + │ + ▼ +근로계약서 생성 + │ + ├──→ v1 PDF (초안) + │ + ├──→ 관리자 서명 → v2 PDF + │ + └──→ 근로자 서명 → v3 PDF (완료) +``` + +### 시나리오 2: 일일 운영 (약 6분) + +``` +공용 태블릿: 얼굴 인식 출퇴근 + │ + ▼ +현장 관리자 로그인 + │ + ├──→ 안전교육 생성 → 관리자 서명 → 참석자 서명 + │ + └──→ 작업일보 작성 → PDF 생성 + │ + ▼ +기업 관리자 로그인 + │ + ├──→ 대시보드 (인원/안전/노무 현황) + │ + ├──→ 안전/작업 문서 조회 + │ + ├──→ 근태 현황 조회 + │ + └──→ 급여 현황 조회 → 명세서 상세 + │ + ▼ +근로자 로그인 + │ + ├──→ 홈 (미결계약, 급여, 근태) + │ + ├──→ 출퇴근 내역 + │ + └──→ 급여 내역 +``` + +--- + +## 기술 하이라이트 (발표 시 강조 포인트) + +### 1. 보안 + +| 기술 | 적용 영역 | 설명 | +|------|----------|------| +| **AES-256-GCM** | 주민등록번호 | 양방향 암호화, 마스킹 조회 | +| **BCrypt** | 비밀번호 | 단방향 해시, Salt 자동 생성 | +| **SHA-256** | 전자서명 | 서명 이미지 무결성 검증 | +| **JWT + HttpOnly** | 인증 토큰 | XSS 방어, Refresh Token Rotation | +| **Timing-Safe Compare** | 해시 비교 | 타이밍 공격 방어 | + +### 2. PDF 생성 + +| 기술 | 설명 | +|------|------| +| **Thymeleaf** | HTML 템플릿 렌더링 | +| **Flying Saucer** | HTML → PDF 변환 | +| **OpenPDF** | PDF 조작 (서명 스탬핑) | +| **S3 Storage** | PDF 버전 관리 (v1, v2, v3) | + +### 3. 얼굴 인식 + +| 기술 | 설명 | +|------|------| +| **Face-api.js** | 프론트엔드 얼굴 검출 | +| **Cosine Similarity** | 얼굴 임베딩 벡터 유사도 | +| **임계값 0.35** | 인식률/오인식률 균형 | + +### 4. 아키텍처 + +| 패턴 | 설명 | +|------|------| +| **Rich Domain Model** | 엔티티에 비즈니스 로직 포함 | +| **Domain-Driven Design** | 도메인별 패키지 구조 | +| **Soft Delete** | 논리 삭제로 데이터 보존 | +| **BaseEntity** | 공통 필드 상속 (createdAt, updatedAt, isDeleted) | + +--- + +## 부록: API 엔드포인트 요약 + +### 인증 (Auth) +``` +POST /v1/auth/register/manager # 현장 관리자 회원가입 +POST /v1/auth/register/employee/step1 # 근로자 회원가입 1단계 +POST /v1/auth/register/employee/step2 # 근로자 회원가입 2단계 +POST /v1/auth/login # 로그인 +POST /v1/auth/refresh # 토큰 갱신 +GET /v1/auth/me # 내 정보 조회 +``` + +### 현장 (Site) +``` +POST /v1/sites # 현장 등록 +GET /v1/sites # 현장 목록 +GET /v1/sites/{siteId} # 현장 상세 +GET /v1/sites/{siteId}/dashboard # 대시보드 +GET /v1/sites/{siteId}/safety-work-documents # 안전/작업 문서 +GET /v1/sites/contract-info # 계약 작성용 정보 +``` + +### 계약 (Contract) +``` +GET /v1/{siteId}/contracts # 계약 목록 +POST /v1/{siteId}/contracts/regular # 상용직 계약 생성 +POST /v1/{siteId}/contracts/daily # 일용직 계약 생성 +POST /v1/{siteId}/contracts/{id}/pdf/initial # PDF 생성 +POST /v1/{siteId}/contracts/{id}/signatures/manager # 관리자 서명 +POST /v1/{siteId}/contracts/{id}/signatures/employee # 근로자 서명 +``` + +### 근태 (Attendance) +``` +POST /v1/attendance/verify # 얼굴 인식 출퇴근 +POST /v1/attendance/my-face # 얼굴 등록 +GET /v1/{siteId}/attendance/records # 근태 현황 +``` + +### 안전교육 (SafetyEducation) +``` +POST /v1/{siteId}/safety-education-logs # 교육 생성 +GET /v1/{siteId}/safety-education-logs # 교육 목록 +GET /v1/{siteId}/safety-education-logs/employees # 대상자 목록 +POST /v1/{siteId}/safety-education-logs/{id}/signatures/manager # 관리자 서명 +POST /v1/{siteId}/safety-education-logs/{id}/signatures/attendee/{empId} # 참석자 서명 +``` + +### 작업일보 (WorkReport) +``` +POST /v1/{siteId}/work-reports # 작업일보 생성 +GET /v1/{siteId}/work-reports # 작업일보 목록 +``` + +### 급여 (Payroll) +``` +GET /v1/payrolls/period/permanent # 상용직 급여 +GET /v1/payrolls/period/daily/monthly # 일용직 월급 +GET /v1/payrolls/period/daily/weekly # 일용직 주급 +GET /v1/payrolls/period/daily/daily # 일용직 일급 +GET /v1/payrolls/{payrollId} # 급여 상세 +``` + +### 사원 (Employee) +``` +GET /sites/{siteId}/employees # 사원 목록 +GET /sites/{siteId}/employees/{id} # 사원 상세 +GET /sites/{siteId}/employees/{id}/contracts # 사원 계약 목록 +GET /sites/{siteId}/employees/{id}/payslips # 사원 급여 목록 +GET /v1/employees/me/home # 근로자 홈 +GET /v1/employees/me/attendance # 근로자 출퇴근 내역 +GET /v1/employees/me/payroll # 근로자 급여 내역 +``` + +--- + +*Build-Up Platform - 건설 현장 인력 관리의 새로운 표준* diff --git a/src/main/java/com/concrete/buildup/domain/document/controller/DocumentController.java b/src/main/java/com/concrete/buildup/domain/document/controller/DocumentController.java index 4fd6d3ec..eeaa4990 100644 --- a/src/main/java/com/concrete/buildup/domain/document/controller/DocumentController.java +++ b/src/main/java/com/concrete/buildup/domain/document/controller/DocumentController.java @@ -24,7 +24,7 @@ */ @Slf4j @RestController -@RequestMapping("/documents") +@RequestMapping("/v1/documents") @RequiredArgsConstructor @Tag(name = "Document", description = "문서 조회 API") public class DocumentController { diff --git a/src/main/java/com/concrete/buildup/domain/employee/controller/EmployeeController.java b/src/main/java/com/concrete/buildup/domain/employee/controller/EmployeeController.java index f5accb87..569bb7a9 100644 --- a/src/main/java/com/concrete/buildup/domain/employee/controller/EmployeeController.java +++ b/src/main/java/com/concrete/buildup/domain/employee/controller/EmployeeController.java @@ -30,7 +30,7 @@ */ @Slf4j @RestController -@RequestMapping("/sites/{siteId}/employees") +@RequestMapping("/v1/sites/{siteId}/employees") @RequiredArgsConstructor @Validated @Tag(name = "Employee", description = "사원 관리 API") From db66da67ffeb20adaf8c5c8fad266e319d59bb58 Mon Sep 17 00:00:00 2001 From: Sewon Kim Date: Mon, 1 Dec 2025 17:54:35 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=B0=B8=EA=B3=A0=EC=9A=A9=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 데이터 SQL 파일 삭제 - 테스트 스크립트 및 로그 파일 삭제 - 임시 문서 파일 삭제 --- BCryptHashGenerator.java | 10 - CONTRIBUTING.md | 492 ---------------- DEMO-SCENARIO.md | 1069 ---------------------------------- DEPLOYMENT-GUIDE.md | 626 -------------------- dashboard-test-data.sql | 127 ---- docker-test-output.log | 0 ec2-additional-test-data.sql | 758 ------------------------ ec2-korean-test-data.sql | 41 -- ec2-test-data-english.sql | 471 --------------- ec2-test-data.sql | 445 -------------- local-schema.sql | 639 -------------------- local-tables-summary.txt | 225 ------- test-api-cookies.sh | 115 ---- test-api-simple.sh | 112 ---- test-api.sh | 111 ---- test-output.log | 312 ---------- 16 files changed, 5553 deletions(-) delete mode 100644 BCryptHashGenerator.java delete mode 100644 CONTRIBUTING.md delete mode 100644 DEMO-SCENARIO.md delete mode 100644 DEPLOYMENT-GUIDE.md delete mode 100644 dashboard-test-data.sql delete mode 100644 docker-test-output.log delete mode 100644 ec2-additional-test-data.sql delete mode 100644 ec2-korean-test-data.sql delete mode 100644 ec2-test-data-english.sql delete mode 100644 ec2-test-data.sql delete mode 100644 local-schema.sql delete mode 100644 local-tables-summary.txt delete mode 100644 test-api-cookies.sh delete mode 100755 test-api-simple.sh delete mode 100755 test-api.sh delete mode 100644 test-output.log diff --git a/BCryptHashGenerator.java b/BCryptHashGenerator.java deleted file mode 100644 index 4320617a..00000000 --- a/BCryptHashGenerator.java +++ /dev/null @@ -1,10 +0,0 @@ -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; - -public class BCryptHashGenerator { - public static void main(String[] args) { - BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String password = "Admin123!@"; - String hash = encoder.encode(password); - System.out.println(hash); - } -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5f95f0ec..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,492 +0,0 @@ -# Contributing to Build-Up Backend - -Build-Up 백엔드 프로젝트에 기여해주셔서 감사합니다! 이 문서는 프로젝트에 기여하는 방법과 개발 워크플로우를 설명합니다. - ---- - -## 📋 목차 - -1. [시작하기](#시작하기) -2. [개발 워크플로우](#개발-워크플로우) -3. [브랜치 전략](#브랜치-전략) -4. [커밋 컨벤션](#커밋-컨벤션) -5. [코드 스타일](#코드-스타일) -6. [테스트 작성](#테스트-작성) -7. [Pull Request 프로세스](#pull-request-프로세스) -8. [코드 리뷰](#코드-리뷰) -9. [문서화](#문서화) -10. [도움말](#도움말) - ---- - -## 🚀 개발 환경 세팅 - -### 1. 사전 준비사항 - -- Java 21 이상 -- IntelliJ IDEA (권장) 또는 Eclipse -- Docker Desktop -- MySQL 클라이언트 (선택사항) -- Git - -### 2. 프로젝트 클론 - -```bash -# 프로젝트 클론 -git clone -cd BU-Server - -# develop 브랜치로 이동 (메인 개발 브랜치) -git checkout develop -``` - -### 3. 환경변수 설정 - -```bash -# .env.example을 복사하여 .env 생성 -cp .env.example .env - -# .env 파일 편집 (필요시) -# 로컬 개발 환경에서는 기본값 사용 가능 -``` - -### 4. 개발 환경 실행 - -**방법 1: 개발 스크립트 사용 (권장)** - -```bash -# 스크립트 실행 권한 부여 (최초 1회) -chmod +x ./start-dev.sh - -# 개발 환경 시작 -./start-dev.sh -``` - -**방법 2: IntelliJ에서 직접 실행** - -1. IntelliJ에서 프로젝트 열기 -2. Active Profile을 `dev`로 설정 -3. `BuildupApplication` 실행 - -### 5. 접속 확인 - -- 애플리케이션: http://localhost:8080/api -- Swagger UI: http://localhost:8080/api/swagger-ui/index.html -- phpMyAdmin: http://localhost:8081 - ---- - -## 🌿 Git 워크플로우 - -### 브랜치 전략 - -``` -main (또는 master) - └── develop (메인 개발 브랜치) - ├── feature/JIRA-123-user-auth (기능 개발) - ├── feature/JIRA-124-employee-list - ├── bugfix/JIRA-125-login-error (버그 수정) - └── hotfix/JIRA-126-critical-fix (긴급 수정) -``` - -### 브랜치 네이밍 규칙 - -| 타입 | 형식 | 예시 | -|------|------|------| -| 기능 개발 | `feature/JIRA-번호-간단한설명` | `feature/JIRA-123-user-auth` | -| 버그 수정 | `bugfix/JIRA-번호-간단한설명` | `bugfix/JIRA-125-login-error` | -| 긴급 수정 | `hotfix/JIRA-번호-간단한설명` | `hotfix/JIRA-126-critical-fix` | - -### 작업 프로세스 - -#### 1. Jira Epic/Story 생성 -- Jira에서 Epic 또는 Story 생성 -- Story를 적절한 크기로 분할 (1-2일 이내 완료 가능한 단위) - -#### 2. GitHub Issue 생성 -- Jira Story를 기반으로 GitHub Issue 생성 -- 제목: `[JIRA-123] 사용자 인증 기능 구현` -- 본문: Jira 링크 및 상세 설명 - -#### 3. 브랜치 생성 및 개발 - -```bash -# develop 브랜치에서 최신 코드 받기 -git checkout develop -git pull origin develop - -# 새 기능 브랜치 생성 -git checkout -b feature/JIRA-123-user-auth - -# 개발 작업 수행... - -# 커밋 (커밋 메시지 규칙 참조) -git add . -git commit -m "feat: [JIRA-123] 사용자 인증 API 구현" - -# 원격 브랜치에 푸시 -git push origin feature/JIRA-123-user-auth -``` - -#### 4. Pull Request 생성 -- GitHub에서 PR 생성 -- PR 템플릿에 따라 작성 -- Reviewer 지정 (팀원) - -#### 5. 코드 리뷰 및 수정 -- 리뷰어는 24시간 이내 리뷰 -- 수정 사항 반영 후 재요청 - -#### 6. 머지 및 브랜치 삭제 -- Approve 받은 후 develop에 머지 -- Squash and Merge 또는 Rebase and Merge 사용 -- 브랜치 삭제 - ---- - -## 💻 코딩 컨벤션 - -### 패키지 구조 - -Domain 중심 구조를 사용합니다: - -``` -com.concrete.buildup/ -├── domain/ -│ └── {domain-name}/ -│ ├── controller/ -│ ├── service/ -│ ├── repository/ -│ ├── dto/ -│ └── entity/ -└── global/ - ├── config/ - ├── exception/ - ├── common/ - └── util/ -``` - -### 네이밍 규칙 - -#### 클래스 -```java -// Controller -public class UserController { } - -// Service -public class UserService { } - -// Repository -public interface UserRepository extends JpaRepository { } - -// DTO -public class UserRequestDto { } -public class UserResponseDto { } - -// Entity -@Entity -public class User { } -``` - -#### 메서드 -```java -// CRUD 메서드 -public UserResponseDto createUser(UserRequestDto request) { } -public UserResponseDto getUserById(Long id) { } -public List getAllUsers() { } -public UserResponseDto updateUser(Long id, UserRequestDto request) { } -public void deleteUser(Long id) { } - -// 조회 메서드 -public List findUsersByName(String name) { } -public Optional findUserByEmail(String email) { } - -// 비즈니스 로직 -public void activateUser(Long userId) { } -public boolean isUserActive(Long userId) { } -``` - -#### 변수 -```java -// camelCase 사용 -private String userName; -private Long userId; -private LocalDateTime createdAt; - -// boolean은 is/has 접두사 -private boolean isActive; -private boolean hasPermission; - -// Collection은 복수형 -private List users; -private Set roles; -``` - -#### 상수 -```java -// UPPER_SNAKE_CASE 사용 -public static final int MAX_RETRY_COUNT = 3; -public static final String DEFAULT_ROLE = "USER"; -``` - -### API 응답 형식 - -모든 API는 `ApiResponse`를 사용합니다: - -```java -// 성공 응답 -// return ResponseEntity.ok(ApiResponse.success(data)); -// return ResponseEntity.ok(ApiResponse.success(data, "조회에 성공했습니다")); - -// 201 Created -// return ResponseEntity - // .status(HttpStatus.CREATED) - // .body(ApiResponse.success(data, "생성되었습니다")); - -// 에러 응답 (GlobalExceptionHandler에서 자동 처리) -// throw new BusinessException(ErrorCode.USER_NOT_FOUND); -``` - -### 어노테이션 순서 - -```java -@RestController -@RequestMapping("/api/users") -@RequiredArgsConstructor -@Slf4j -public class UserController { - - @GetMapping("/{id}") - @Operation(summary = "사용자 조회") - public ResponseEntity> getUser( - @PathVariable Long id - ) { - // ... - } -} -``` - ---- - -## 📝 커밋 메시지 규칙 - -### 형식 - -``` -: [JIRA-번호] - - (선택사항) - -