From f8427ee1d00aeced442d8406cb592ede4123e8de Mon Sep 17 00:00:00 2001 From: JAE SIK CHO Date: Thu, 9 Apr 2026 11:12:12 +0900 Subject: [PATCH] share job --- .env | 26 + .env.example | 10 + DB Scheme | 6516 +++++++++++++++++ Jenkinsfile | 96 + MIGRATION_GUIDE.md | 339 + TASK_LIST.md | 334 + backend/.gitignore | 10 + backend/Dockerfile | 9 + backend/build.gradle | 92 + backend/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + backend/gradlew | 251 + backend/gradlew.bat | 94 + backend/settings.gradle | 1 + .../com/company/gw/GwBackendApplication.java | 16 + .../gw/board/controller/BoardController.java | 115 + .../company/gw/board/mapper/BoardMapper.java | 40 + .../gw/board/service/BoardService.java | 228 + .../gw/common/config/JacksonConfig.java | 69 + .../gw/common/config/MyBatisConfig.java | 53 + .../company/gw/common/config/RedisConfig.java | 61 + .../gw/common/config/SecurityConfig.java | 85 + .../common/controller/AttachController.java | 133 + .../gw/common/controller/AuthController.java | 88 + .../gw/common/controller/CodeController.java | 78 + .../gw/common/controller/ExcelController.java | 116 + .../company/gw/common/dto/ApiResponse.java | 46 + .../com/company/gw/common/dto/AttachDto.java | 16 + .../gw/common/dto/ControllerRoleDto.java | 12 + .../company/gw/common/dto/CurrentUser.java | 24 + .../gw/common/dto/LoginRequestDto.java | 17 + .../gw/common/dto/LoginResponseDto.java | 27 + .../com/company/gw/common/dto/MenuDto.java | 18 + .../gw/common/dto/RefreshRequestDto.java | 11 + .../com/company/gw/common/dto/UserDto.java | 19 + .../gw/common/exception/BizException.java | 20 + .../exception/GlobalExceptionHandler.java | 42 + .../filter/JwtAuthenticationFilter.java | 71 + .../gw/common/mapper/AttachMapper.java | 24 + .../company/gw/common/mapper/CodeMapper.java | 18 + .../gw/common/mapper/CommonMapper.java | 12 + .../company/gw/common/mapper/MenuMapper.java | 17 + .../gw/common/mapper/SecurityMapper.java | 13 + .../company/gw/common/mapper/UserMapper.java | 15 + .../gw/common/service/AttachService.java | 226 + .../gw/common/service/AuthService.java | 189 + .../gw/common/service/CodeService.java | 72 + .../service/CustomUserDetailsService.java | 41 + .../gw/common/service/SequenceService.java | 24 + .../com/company/gw/common/util/ExcelUtil.java | 86 + .../com/company/gw/common/util/JwtUtil.java | 82 + .../company/gw/common/util/PasswordUtil.java | 30 + .../company/gw/common/util/SecurityUtil.java | 52 + .../controller/CodeManageController.java | 62 + .../controller/MenuManageController.java | 89 + .../controller/UserManageController.java | 95 + .../envset/controller/WorkCdController.java | 58 + .../company/gw/envset/dto/AuthMenuDto.java | 23 + .../com/company/gw/envset/dto/CodeDto.java | 18 + .../company/gw/envset/dto/CodeIndexDto.java | 18 + .../company/gw/envset/dto/MenuManageDto.java | 22 + .../company/gw/envset/dto/UserAuthDto.java | 17 + .../company/gw/envset/dto/UserDetailDto.java | 31 + .../company/gw/envset/dto/UserListDto.java | 15 + .../company/gw/envset/dto/UserSaveDto.java | 39 + .../gw/envset/mapper/CodeManageMapper.java | 25 + .../gw/envset/mapper/MenuManageMapper.java | 33 + .../gw/envset/mapper/UserManageMapper.java | 26 + .../gw/envset/mapper/WorkCdMapper.java | 13 + .../gw/envset/service/CodeManageService.java | 71 + .../gw/envset/service/MenuManageService.java | 102 + .../gw/envset/service/UserManageService.java | 117 + .../gw/envset/service/WorkCdService.java | 47 + .../gw/fedex/controller/FedexController.java | 58 + .../company/gw/fedex/mapper/FedexMapper.java | 17 + .../gw/fedex/service/FedexService.java | 60 + .../gw/main/controller/MainController.java | 69 + .../company/gw/main/mapper/MainMapper.java | 23 + .../company/gw/main/service/MainService.java | 221 + .../gw/tam/controller/TamController.java | 193 + .../company/gw/tam/mapper/ApvdocMapper.java | 52 + .../com/company/gw/tam/mapper/TamMapper.java | 43 + .../company/gw/tam/service/ApvdocService.java | 262 + .../company/gw/tam/service/TamService.java | 315 + .../gw/wplan/controller/WplanController.java | 81 + .../company/gw/wplan/mapper/WplanMapper.java | 27 + .../gw/wplan/service/WplanService.java | 97 + .../gw/wtime/controller/WtimeController.java | 58 + .../company/gw/wtime/mapper/WtimeMapper.java | 16 + .../gw/wtime/service/WtimeService.java | 59 + .../main/resources/application-mariadb.yml | 17 + .../src/main/resources/application-mssql.yml | 13 + .../src/main/resources/application-oracle.yml | 17 + backend/src/main/resources/application.yml | 62 + backend/src/main/resources/logback-spring.xml | 85 + .../main/resources/mapper/board/board_sql.xml | 339 + .../resources/mapper/common/attach_sql.xml | 111 + .../main/resources/mapper/common/code_sql.xml | 145 + .../resources/mapper/common/common_sql.xml | 28 + .../main/resources/mapper/common/menu_sql.xml | 146 + .../resources/mapper/common/security_sql.xml | 18 + .../main/resources/mapper/common/user_sql.xml | 41 + .../mapper/envset/envset0010_sql.xml | 246 + .../mapper/envset/envset0020_sql.xml | 158 + .../mapper/envset/envset0040_sql.xml | 167 + .../mapper/envset/envset0050_sql.xml | 213 + .../main/resources/mapper/fedex/fedex_sql.xml | 125 + .../main/resources/mapper/main/main_sql.xml | 340 + .../main/resources/mapper/tam/apvdoc_sql.xml | 321 + .../src/main/resources/mapper/tam/tam_sql.xml | 417 ++ .../main/resources/mapper/wplan/wplan_sql.xml | 287 + .../main/resources/mapper/wtime/wtime_sql.xml | 247 + backend/tls-local.security | 2 + docker-compose.dev.yml | 34 + docker-compose.yml | 101 + frontend/.env.example | 1 + frontend/.gitignore | 7 + frontend/Dockerfile | 23 + frontend/next-env.d.ts | 5 + frontend/next.config.ts | 55 + frontend/package.json | 49 + frontend/postcss.config.mjs | 8 + .../board/[boardType]/[postSno]/edit/page.tsx | 24 + .../board/[boardType]/[postSno]/page.tsx | 178 + .../src/app/(main)/board/[boardType]/page.tsx | 158 + .../(main)/board/[boardType]/write/page.tsx | 7 + frontend/src/app/(main)/dashboard/page.tsx | 500 ++ .../src/app/(main)/envset/codes-view/page.tsx | 115 + frontend/src/app/(main)/envset/codes/page.tsx | 205 + frontend/src/app/(main)/envset/layout.tsx | 32 + frontend/src/app/(main)/envset/menus/page.tsx | 143 + frontend/src/app/(main)/envset/page.tsx | 5 + .../src/app/(main)/envset/user-auth/page.tsx | 135 + frontend/src/app/(main)/envset/users/page.tsx | 234 + .../src/app/(main)/envset/workcd/page.tsx | 292 + .../src/app/(main)/fedex/0010/[sq]/page.tsx | 107 + frontend/src/app/(main)/fedex/0010/page.tsx | 121 + .../src/app/(main)/fedex/0010/write/page.tsx | 187 + frontend/src/app/(main)/layout.tsx | 16 + frontend/src/app/(main)/loading.tsx | 10 + frontend/src/app/(main)/my/change-pw/page.tsx | 109 + frontend/src/app/(main)/tam/0010/page.tsx | 187 + .../src/app/(main)/tam/0020/[docId]/page.tsx | 99 + .../src/app/(main)/tam/0020/list/page.tsx | 179 + frontend/src/app/(main)/tam/0020/page.tsx | 28 + .../src/app/(main)/tam/0020/write/page.tsx | 14 + .../src/app/(main)/tam/0030/[docId]/page.tsx | 154 + frontend/src/app/(main)/tam/0030/page.tsx | 248 + frontend/src/app/(main)/tam/0040/page.tsx | 123 + frontend/src/app/(main)/wplan/0010/page.tsx | 211 + frontend/src/app/(main)/wplan/0020/page.tsx | 124 + frontend/src/app/(main)/wplan/0030/page.tsx | 237 + frontend/src/app/(main)/wtime/0010/page.tsx | 143 + frontend/src/app/(main)/wtime/0030/page.tsx | 134 + frontend/src/app/error.tsx | 32 + frontend/src/app/globals.css | 19 + frontend/src/app/layout.tsx | 18 + frontend/src/app/login/page.tsx | 17 + frontend/src/app/not-found.tsx | 19 + frontend/src/app/page.tsx | 5 + frontend/src/components/auth/LoginForm.tsx | 167 + frontend/src/components/board/PostForm.tsx | 109 + frontend/src/components/common/FileUpload.tsx | 161 + frontend/src/components/common/Modal.tsx | 78 + frontend/src/components/common/Pagination.tsx | 81 + frontend/src/components/common/Providers.tsx | 26 + frontend/src/components/common/RichEditor.tsx | 53 + frontend/src/components/common/Toast.tsx | 109 + frontend/src/components/layout/Breadcrumb.tsx | 47 + frontend/src/components/layout/Header.tsx | 49 + frontend/src/components/layout/Sidebar.tsx | 204 + .../src/components/tam/ApproverSection.tsx | 207 + frontend/src/components/tam/ApvdocInfo.tsx | 125 + frontend/src/components/tam/ApvreqForm.tsx | 129 + frontend/src/lib/api/attach.ts | 117 + frontend/src/lib/api/auth.ts | 17 + frontend/src/lib/api/board.ts | 119 + frontend/src/lib/api/client.ts | 51 + frontend/src/lib/api/envset.ts | 120 + frontend/src/lib/api/fedex.ts | 70 + frontend/src/lib/api/main.ts | 82 + frontend/src/lib/api/tam.ts | 198 + frontend/src/lib/api/wplan.ts | 89 + frontend/src/lib/api/wtime.ts | 89 + frontend/src/lib/store/authStore.ts | 53 + frontend/src/lib/utils/date.ts | 18 + frontend/src/middleware.ts | 24 + frontend/src/types/auth.ts | 41 + frontend/src/types/common.ts | 18 + frontend/tailwind.config.ts | 24 + frontend/tsconfig.json | 23 + nginx/nginx.conf | 72 + .../e5ac8527d2834594aa9c299bf69ef2a8.pdf | Bin 0 -> 421322 bytes 193 files changed, 23830 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 DB Scheme create mode 100644 Jenkinsfile create mode 100644 MIGRATION_GUIDE.md create mode 100644 TASK_LIST.md create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/build.gradle create mode 100644 backend/gradle/wrapper/gradle-wrapper.jar create mode 100644 backend/gradle/wrapper/gradle-wrapper.properties create mode 100644 backend/gradlew create mode 100644 backend/gradlew.bat create mode 100644 backend/settings.gradle create mode 100644 backend/src/main/java/com/company/gw/GwBackendApplication.java create mode 100644 backend/src/main/java/com/company/gw/board/controller/BoardController.java create mode 100644 backend/src/main/java/com/company/gw/board/mapper/BoardMapper.java create mode 100644 backend/src/main/java/com/company/gw/board/service/BoardService.java create mode 100644 backend/src/main/java/com/company/gw/common/config/JacksonConfig.java create mode 100644 backend/src/main/java/com/company/gw/common/config/MyBatisConfig.java create mode 100644 backend/src/main/java/com/company/gw/common/config/RedisConfig.java create mode 100644 backend/src/main/java/com/company/gw/common/config/SecurityConfig.java create mode 100644 backend/src/main/java/com/company/gw/common/controller/AttachController.java create mode 100644 backend/src/main/java/com/company/gw/common/controller/AuthController.java create mode 100644 backend/src/main/java/com/company/gw/common/controller/CodeController.java create mode 100644 backend/src/main/java/com/company/gw/common/controller/ExcelController.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/ApiResponse.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/AttachDto.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/ControllerRoleDto.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/CurrentUser.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/LoginRequestDto.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/LoginResponseDto.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/MenuDto.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/RefreshRequestDto.java create mode 100644 backend/src/main/java/com/company/gw/common/dto/UserDto.java create mode 100644 backend/src/main/java/com/company/gw/common/exception/BizException.java create mode 100644 backend/src/main/java/com/company/gw/common/exception/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/company/gw/common/filter/JwtAuthenticationFilter.java create mode 100644 backend/src/main/java/com/company/gw/common/mapper/AttachMapper.java create mode 100644 backend/src/main/java/com/company/gw/common/mapper/CodeMapper.java create mode 100644 backend/src/main/java/com/company/gw/common/mapper/CommonMapper.java create mode 100644 backend/src/main/java/com/company/gw/common/mapper/MenuMapper.java create mode 100644 backend/src/main/java/com/company/gw/common/mapper/SecurityMapper.java create mode 100644 backend/src/main/java/com/company/gw/common/mapper/UserMapper.java create mode 100644 backend/src/main/java/com/company/gw/common/service/AttachService.java create mode 100644 backend/src/main/java/com/company/gw/common/service/AuthService.java create mode 100644 backend/src/main/java/com/company/gw/common/service/CodeService.java create mode 100644 backend/src/main/java/com/company/gw/common/service/CustomUserDetailsService.java create mode 100644 backend/src/main/java/com/company/gw/common/service/SequenceService.java create mode 100644 backend/src/main/java/com/company/gw/common/util/ExcelUtil.java create mode 100644 backend/src/main/java/com/company/gw/common/util/JwtUtil.java create mode 100644 backend/src/main/java/com/company/gw/common/util/PasswordUtil.java create mode 100644 backend/src/main/java/com/company/gw/common/util/SecurityUtil.java create mode 100644 backend/src/main/java/com/company/gw/envset/controller/CodeManageController.java create mode 100644 backend/src/main/java/com/company/gw/envset/controller/MenuManageController.java create mode 100644 backend/src/main/java/com/company/gw/envset/controller/UserManageController.java create mode 100644 backend/src/main/java/com/company/gw/envset/controller/WorkCdController.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/AuthMenuDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/CodeDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/CodeIndexDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/MenuManageDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/UserAuthDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/UserDetailDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/UserListDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/dto/UserSaveDto.java create mode 100644 backend/src/main/java/com/company/gw/envset/mapper/CodeManageMapper.java create mode 100644 backend/src/main/java/com/company/gw/envset/mapper/MenuManageMapper.java create mode 100644 backend/src/main/java/com/company/gw/envset/mapper/UserManageMapper.java create mode 100644 backend/src/main/java/com/company/gw/envset/mapper/WorkCdMapper.java create mode 100644 backend/src/main/java/com/company/gw/envset/service/CodeManageService.java create mode 100644 backend/src/main/java/com/company/gw/envset/service/MenuManageService.java create mode 100644 backend/src/main/java/com/company/gw/envset/service/UserManageService.java create mode 100644 backend/src/main/java/com/company/gw/envset/service/WorkCdService.java create mode 100644 backend/src/main/java/com/company/gw/fedex/controller/FedexController.java create mode 100644 backend/src/main/java/com/company/gw/fedex/mapper/FedexMapper.java create mode 100644 backend/src/main/java/com/company/gw/fedex/service/FedexService.java create mode 100644 backend/src/main/java/com/company/gw/main/controller/MainController.java create mode 100644 backend/src/main/java/com/company/gw/main/mapper/MainMapper.java create mode 100644 backend/src/main/java/com/company/gw/main/service/MainService.java create mode 100644 backend/src/main/java/com/company/gw/tam/controller/TamController.java create mode 100644 backend/src/main/java/com/company/gw/tam/mapper/ApvdocMapper.java create mode 100644 backend/src/main/java/com/company/gw/tam/mapper/TamMapper.java create mode 100644 backend/src/main/java/com/company/gw/tam/service/ApvdocService.java create mode 100644 backend/src/main/java/com/company/gw/tam/service/TamService.java create mode 100644 backend/src/main/java/com/company/gw/wplan/controller/WplanController.java create mode 100644 backend/src/main/java/com/company/gw/wplan/mapper/WplanMapper.java create mode 100644 backend/src/main/java/com/company/gw/wplan/service/WplanService.java create mode 100644 backend/src/main/java/com/company/gw/wtime/controller/WtimeController.java create mode 100644 backend/src/main/java/com/company/gw/wtime/mapper/WtimeMapper.java create mode 100644 backend/src/main/java/com/company/gw/wtime/service/WtimeService.java create mode 100644 backend/src/main/resources/application-mariadb.yml create mode 100644 backend/src/main/resources/application-mssql.yml create mode 100644 backend/src/main/resources/application-oracle.yml create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/logback-spring.xml create mode 100644 backend/src/main/resources/mapper/board/board_sql.xml create mode 100644 backend/src/main/resources/mapper/common/attach_sql.xml create mode 100644 backend/src/main/resources/mapper/common/code_sql.xml create mode 100644 backend/src/main/resources/mapper/common/common_sql.xml create mode 100644 backend/src/main/resources/mapper/common/menu_sql.xml create mode 100644 backend/src/main/resources/mapper/common/security_sql.xml create mode 100644 backend/src/main/resources/mapper/common/user_sql.xml create mode 100644 backend/src/main/resources/mapper/envset/envset0010_sql.xml create mode 100644 backend/src/main/resources/mapper/envset/envset0020_sql.xml create mode 100644 backend/src/main/resources/mapper/envset/envset0040_sql.xml create mode 100644 backend/src/main/resources/mapper/envset/envset0050_sql.xml create mode 100644 backend/src/main/resources/mapper/fedex/fedex_sql.xml create mode 100644 backend/src/main/resources/mapper/main/main_sql.xml create mode 100644 backend/src/main/resources/mapper/tam/apvdoc_sql.xml create mode 100644 backend/src/main/resources/mapper/tam/tam_sql.xml create mode 100644 backend/src/main/resources/mapper/wplan/wplan_sql.xml create mode 100644 backend/src/main/resources/mapper/wtime/wtime_sql.xml create mode 100644 backend/tls-local.security create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.ts create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/src/app/(main)/board/[boardType]/[postSno]/edit/page.tsx create mode 100644 frontend/src/app/(main)/board/[boardType]/[postSno]/page.tsx create mode 100644 frontend/src/app/(main)/board/[boardType]/page.tsx create mode 100644 frontend/src/app/(main)/board/[boardType]/write/page.tsx create mode 100644 frontend/src/app/(main)/dashboard/page.tsx create mode 100644 frontend/src/app/(main)/envset/codes-view/page.tsx create mode 100644 frontend/src/app/(main)/envset/codes/page.tsx create mode 100644 frontend/src/app/(main)/envset/layout.tsx create mode 100644 frontend/src/app/(main)/envset/menus/page.tsx create mode 100644 frontend/src/app/(main)/envset/page.tsx create mode 100644 frontend/src/app/(main)/envset/user-auth/page.tsx create mode 100644 frontend/src/app/(main)/envset/users/page.tsx create mode 100644 frontend/src/app/(main)/envset/workcd/page.tsx create mode 100644 frontend/src/app/(main)/fedex/0010/[sq]/page.tsx create mode 100644 frontend/src/app/(main)/fedex/0010/page.tsx create mode 100644 frontend/src/app/(main)/fedex/0010/write/page.tsx create mode 100644 frontend/src/app/(main)/layout.tsx create mode 100644 frontend/src/app/(main)/loading.tsx create mode 100644 frontend/src/app/(main)/my/change-pw/page.tsx create mode 100644 frontend/src/app/(main)/tam/0010/page.tsx create mode 100644 frontend/src/app/(main)/tam/0020/[docId]/page.tsx create mode 100644 frontend/src/app/(main)/tam/0020/list/page.tsx create mode 100644 frontend/src/app/(main)/tam/0020/page.tsx create mode 100644 frontend/src/app/(main)/tam/0020/write/page.tsx create mode 100644 frontend/src/app/(main)/tam/0030/[docId]/page.tsx create mode 100644 frontend/src/app/(main)/tam/0030/page.tsx create mode 100644 frontend/src/app/(main)/tam/0040/page.tsx create mode 100644 frontend/src/app/(main)/wplan/0010/page.tsx create mode 100644 frontend/src/app/(main)/wplan/0020/page.tsx create mode 100644 frontend/src/app/(main)/wplan/0030/page.tsx create mode 100644 frontend/src/app/(main)/wtime/0010/page.tsx create mode 100644 frontend/src/app/(main)/wtime/0030/page.tsx create mode 100644 frontend/src/app/error.tsx create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/login/page.tsx create mode 100644 frontend/src/app/not-found.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/components/auth/LoginForm.tsx create mode 100644 frontend/src/components/board/PostForm.tsx create mode 100644 frontend/src/components/common/FileUpload.tsx create mode 100644 frontend/src/components/common/Modal.tsx create mode 100644 frontend/src/components/common/Pagination.tsx create mode 100644 frontend/src/components/common/Providers.tsx create mode 100644 frontend/src/components/common/RichEditor.tsx create mode 100644 frontend/src/components/common/Toast.tsx create mode 100644 frontend/src/components/layout/Breadcrumb.tsx create mode 100644 frontend/src/components/layout/Header.tsx create mode 100644 frontend/src/components/layout/Sidebar.tsx create mode 100644 frontend/src/components/tam/ApproverSection.tsx create mode 100644 frontend/src/components/tam/ApvdocInfo.tsx create mode 100644 frontend/src/components/tam/ApvreqForm.tsx create mode 100644 frontend/src/lib/api/attach.ts create mode 100644 frontend/src/lib/api/auth.ts create mode 100644 frontend/src/lib/api/board.ts create mode 100644 frontend/src/lib/api/client.ts create mode 100644 frontend/src/lib/api/envset.ts create mode 100644 frontend/src/lib/api/fedex.ts create mode 100644 frontend/src/lib/api/main.ts create mode 100644 frontend/src/lib/api/tam.ts create mode 100644 frontend/src/lib/api/wplan.ts create mode 100644 frontend/src/lib/api/wtime.ts create mode 100644 frontend/src/lib/store/authStore.ts create mode 100644 frontend/src/lib/utils/date.ts create mode 100644 frontend/src/middleware.ts create mode 100644 frontend/src/types/auth.ts create mode 100644 frontend/src/types/common.ts create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 nginx/nginx.conf create mode 100644 uploads/board_0002/20260330/e5ac8527d2834594aa9c299bf69ef2a8.pdf diff --git a/.env b/.env new file mode 100644 index 0000000..fe992b6 --- /dev/null +++ b/.env @@ -0,0 +1,26 @@ +# ─────────── DB (MSSQL) ─────────── +DB_URL=jdbc:sqlserver://121.156.116.136:52785;databaseName=logins_test;encrypt=false;trustServerCertificate=true +DB_USERNAME=logins +DB_PASSWORD=ghkfkdahrfh40-8 + +# ─────────── JWT ─────────── +# 256비트 이상 랜덤 시크릿 (운영 시 반드시 교체) +JWT_SECRET=change-me-to-a-random-256bit-or-longer-secret-key-for-production + +# ─────────── Redis ─────────── +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# ─────────── RabbitMQ ─────────── +RABBITMQ_HOST=rabbitmq +RABBITMQ_PORT=5672 +RABBITMQ_USER=guest +RABBITMQ_PASSWORD=guest + +# ─────────── File Upload ─────────── +FILE_UPLOAD_PATH=/data/uploads + +# ─────────── Spring Profile ─────────── +# mssql | mariadb +SPRING_PROFILES_ACTIVE=mssql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8c9c8fd --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Oracle DB +DB_URL=jdbc:oracle:thin:@host:1521:SID +DB_USERNAME= +DB_PASSWORD= + +# JWT +JWT_SECRET=your-secret-key-must-be-at-least-256-bits-long + +# File Storage +FILE_UPLOAD_PATH=/data/uploads diff --git a/DB Scheme b/DB Scheme new file mode 100644 index 0000000..7b26bfd --- /dev/null +++ b/DB Scheme @@ -0,0 +1,6516 @@ +USE [master] +GO +/****** Object: Database [logins_test] Script Date: 2026-03-30 오후 1:12:18 ******/ +CREATE DATABASE [logins_test] + CONTAINMENT = NONE + ON PRIMARY +( NAME = N'gw', FILENAME = N'D:\LOGINS\Database\Groupware\logins_test.mdf' , SIZE = 94720KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB ) + LOG ON +( NAME = N'gw_log', FILENAME = N'D:\LOGINS\Database\Groupware\logins_test_log.ldf' , SIZE = 3136KB , MAXSIZE = 2048GB , FILEGROWTH = 10%) +GO +ALTER DATABASE [logins_test] SET COMPATIBILITY_LEVEL = 110 +GO +IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled')) +begin +EXEC [logins_test].[dbo].[sp_fulltext_database] @action = 'enable' +end +GO +ALTER DATABASE [logins_test] SET ANSI_NULL_DEFAULT OFF +GO +ALTER DATABASE [logins_test] SET ANSI_NULLS OFF +GO +ALTER DATABASE [logins_test] SET ANSI_PADDING OFF +GO +ALTER DATABASE [logins_test] SET ANSI_WARNINGS OFF +GO +ALTER DATABASE [logins_test] SET ARITHABORT OFF +GO +ALTER DATABASE [logins_test] SET AUTO_CLOSE OFF +GO +ALTER DATABASE [logins_test] SET AUTO_SHRINK OFF +GO +ALTER DATABASE [logins_test] SET AUTO_UPDATE_STATISTICS ON +GO +ALTER DATABASE [logins_test] SET CURSOR_CLOSE_ON_COMMIT OFF +GO +ALTER DATABASE [logins_test] SET CURSOR_DEFAULT GLOBAL +GO +ALTER DATABASE [logins_test] SET CONCAT_NULL_YIELDS_NULL OFF +GO +ALTER DATABASE [logins_test] SET NUMERIC_ROUNDABORT OFF +GO +ALTER DATABASE [logins_test] SET QUOTED_IDENTIFIER OFF +GO +ALTER DATABASE [logins_test] SET RECURSIVE_TRIGGERS OFF +GO +ALTER DATABASE [logins_test] SET DISABLE_BROKER +GO +ALTER DATABASE [logins_test] SET AUTO_UPDATE_STATISTICS_ASYNC OFF +GO +ALTER DATABASE [logins_test] SET DATE_CORRELATION_OPTIMIZATION OFF +GO +ALTER DATABASE [logins_test] SET TRUSTWORTHY OFF +GO +ALTER DATABASE [logins_test] SET ALLOW_SNAPSHOT_ISOLATION OFF +GO +ALTER DATABASE [logins_test] SET PARAMETERIZATION SIMPLE +GO +ALTER DATABASE [logins_test] SET READ_COMMITTED_SNAPSHOT OFF +GO +ALTER DATABASE [logins_test] SET HONOR_BROKER_PRIORITY OFF +GO +ALTER DATABASE [logins_test] SET RECOVERY FULL +GO +ALTER DATABASE [logins_test] SET MULTI_USER +GO +ALTER DATABASE [logins_test] SET PAGE_VERIFY CHECKSUM +GO +ALTER DATABASE [logins_test] SET DB_CHAINING OFF +GO +ALTER DATABASE [logins_test] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF ) +GO +ALTER DATABASE [logins_test] SET TARGET_RECOVERY_TIME = 0 SECONDS +GO +ALTER DATABASE [logins_test] SET DELAYED_DURABILITY = DISABLED +GO +EXEC sys.sp_db_vardecimal_storage_format N'logins_test', N'ON' +GO +USE [logins_test] +GO +/****** Object: User [logins] Script Date: 2026-03-30 오후 1:12:19 ******/ +CREATE USER [logins] FOR LOGIN [logins] WITH DEFAULT_SCHEMA=[dbo] +GO +ALTER ROLE [db_owner] ADD MEMBER [logins] +GO +/****** Object: UserDefinedTableType [dbo].[ApprovalParamTableType] Script Date: 2026-03-30 오후 1:12:19 ******/ +CREATE TYPE [dbo].[ApprovalParamTableType] AS TABLE( + [CORP_NO] [varchar](15) NULL, + [APRVL_DOC_ID] [int] NULL, + [USR_ID] [varchar](10) NULL +) +GO +/****** Object: UserDefinedTableType [dbo].[TableType] Script Date: 2026-03-30 오후 1:12:19 ******/ +CREATE TYPE [dbo].[TableType] AS TABLE( + [CORP_NO] [varchar](15) NULL, + [APRVL_DOC_ID] [int] NULL, + [USR_ID] [varchar](10) NULL +) +GO +/****** Object: Table [dbo].[bfile] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[bfile]( + [ID] [int] NOT NULL, + [b_id] [int] NULL, + [filename] [nvarchar](50) NULL, + [filesize] [int] NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[bment] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[bment]( + [ID] [int] NOT NULL, + [b_id] [int] NULL, + [m_name] [nvarchar](50) NULL, + [m_ment] [ntext] NULL, + [m_date] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[board] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[board]( + [ID] [int] NOT NULL, + [title] [nvarchar](250) NULL, + [content] [ntext] NULL, + [date] [nvarchar](50) NULL, + [name] [nvarchar](50) NULL, + [count] [int] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[bphoto] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[bphoto]( + [ID] [int] NOT NULL, + [b_id] [int] NULL, + [filename] [nvarchar](50) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[confile] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[confile]( + [ID] [int] NOT NULL, + [n_id] [int] NULL, + [filename] [nvarchar](50) NULL, + [filesize] [int] NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[conotice] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[conotice]( + [ID] [int] NOT NULL, + [title] [nvarchar](250) NULL, + [content] [ntext] NULL, + [date] [nvarchar](50) NULL, + [name] [nvarchar](50) NULL, + [count] [int] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[dummy] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[dummy]( + [idx] [int] IDENTITY(1,1) NOT NULL, + [NM] [varchar](50) NULL, + [VAL] [varchar](50) NULL, + [CD] [varchar](50) NULL, + CONSTRAINT [PK_dummy] PRIMARY KEY CLUSTERED +( + [idx] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[errorMng] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[errorMng]( + [sq] [bigint] IDENTITY(1,1) NOT NULL, + [ie_gbn] [varchar](1) NULL, + [s_code] [varchar](5) NULL, + [s_year] [varchar](2) NULL, + [s_jechl] [varchar](7) NULL, + [com_nm] [varchar](28) NULL, + [j_singo_dt] [varchar](8) NULL, + [s_singo_dt] [varchar](8) NULL, + [j_cd_cd] [int] NULL, + [j_title_cd] [int] NULL, + [j_contents] [varchar](200) NULL, + [j_gwi_cd] [int] NULL, + [j_gwi_user_id1] [varchar](50) NULL, + [j_gwi_user_id2] [varchar](50) NULL, + [reg_user_id] [varchar](50) NULL, + [f_st_cd] [int] NULL, + [f_jj_cd] [int] NULL, + [f_jj_contents] [text] NULL, + [f_jj_nm] [varchar](10) NULL, + [f_jj_reg_dt] [varchar](8) NULL, + [f_jj_reg_gbn] [varchar](1) NULL, + [u_title] [varchar](50) NULL, + [u_res] [varchar](400) NULL, + [u_reg_dt] [varchar](8) NULL, + [ATTACH_NO] [varchar](50) NULL, + [work_dt] [datetime] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[exco] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[exco]( + [ID] [int] NOT NULL, + [singono] [nvarchar](20) NULL, + [importer] [nvarchar](50) NULL, + [singodate] [nvarchar](10) NULL, + [sclass] [nvarchar](50) NULL, + [content] [ntext] NULL, + [name] [nvarchar](50) NULL, + [date] [nvarchar](50) NULL, + [stats] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[f_jj_gbn] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[f_jj_gbn]( + [f_jj_cd] [bigint] IDENTITY(1,1) NOT NULL, + [f_jj_des1] [varchar](2) NULL, + [f_jj_des2] [varchar](50) NULL, + [f_jj_des3] [varchar](100) NULL, + CONSTRAINT [PK_f_jj_gbn] PRIMARY KEY CLUSTERED +( + [f_jj_cd] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[f_jj_st] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[f_jj_st]( + [f_st_cd] [int] IDENTITY(1,1) NOT NULL, + [f_st_des] [varchar](10) NULL, + CONSTRAINT [PK_f_jj_st] PRIMARY KEY CLUSTERED +( + [f_st_cd] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[gwi_gbn] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[gwi_gbn]( + [j_gwi_cd] [int] IDENTITY(1,1) NOT NULL, + [j_gwi_des] [varchar](10) NULL, + CONSTRAINT [PK_f_gwi_gbn] PRIMARY KEY CLUSTERED +( + [j_gwi_cd] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[holiday] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[holiday]( + [ID] [int] NOT NULL, + [날짜] [nvarchar](50) NULL, + [설명] [nvarchar](50) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[inco] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[inco]( + [ID] [int] NOT NULL, + [singono] [nvarchar](20) NULL, + [importer] [nvarchar](50) NULL, + [singodate] [nvarchar](10) NULL, + [sclass] [nvarchar](50) NULL, + [scause] [nvarchar](50) NULL, + [content] [ntext] NULL, + [name] [nvarchar](50) NULL, + [date] [nvarchar](50) NULL, + [stats] [nvarchar](50) NULL, + [statsdate] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[incolog] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[incolog]( + [ID] [int] NOT NULL, + [i_id] [int] NULL, + [ltitle] [nvarchar](50) NULL, + [ltime] [nvarchar](50) NULL, + [lment] [nvarchar](200) NULL, + [lwriter] [nvarchar](50) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[incoment] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[incoment]( + [ID] [int] NOT NULL, + [i_id] [int] NULL, + [m_name] [nvarchar](50) NULL, + [m_ment] [ntext] NULL, + [m_date] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[jj_cd] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[jj_cd]( + [j_cd_cd] [int] IDENTITY(1,1) NOT NULL, + [j_cd_des] [varchar](10) NULL, + CONSTRAINT [PK_f_jj_cd] PRIMARY KEY CLUSTERED +( + [j_cd_cd] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[jj_contents] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[jj_contents]( + [j_title_cd] [int] IDENTITY(1,1) NOT NULL, + [j_title_des] [varchar](20) NULL, + CONSTRAINT [PK_f_jj_contents] PRIMARY KEY CLUSTERED +( + [j_title_cd] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[kfile] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[kfile]( + [ID] [int] NOT NULL, + [b_id] [int] NULL, + [filename] [nvarchar](50) NULL, + [filesize] [int] NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[kment] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[kment]( + [ID] [int] NOT NULL, + [b_id] [int] NULL, + [m_name] [nvarchar](50) NULL, + [m_ment] [ntext] NULL, + [m_date] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[know] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[know]( + [ID] [int] NOT NULL, + [title] [nvarchar](250) NULL, + [content] [ntext] NULL, + [date] [nvarchar](50) NULL, + [name] [nvarchar](50) NULL, + [count] [int] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[kphoto] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[kphoto]( + [ID] [int] NOT NULL, + [b_id] [int] NULL, + [filename] [nvarchar](50) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[latereport] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[latereport]( + [ID] [int] NOT NULL, + [name] [nvarchar](255) NULL, + [reason] [ntext] NULL, + [timein] [nvarchar](255) NULL, + [latetime] [int] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[lawfile] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[lawfile]( + [ID] [int] NOT NULL, + [법] [nvarchar](50) NULL, + [이름] [nvarchar](50) NULL, + [파일명] [nvarchar](50) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[nfile] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[nfile]( + [ID] [int] NOT NULL, + [n_id] [int] NULL, + [filename] [nvarchar](50) NULL, + [filesize] [int] NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[nment] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[nment]( + [ID] [int] NOT NULL, + [n_id] [int] NULL, + [m_name] [nvarchar](50) NULL, + [m_ment] [ntext] NULL, + [m_date] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[notice] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[notice]( + [ID] [int] NOT NULL, + [title] [nvarchar](250) NULL, + [content] [ntext] NULL, + [date] [nvarchar](50) NULL, + [name] [nvarchar](50) NULL, + [count] [int] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[notwork] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[notwork]( + [ID] [int] NOT NULL, + [name] [nvarchar](50) NULL, + [date] [nvarchar](10) NULL, + [reason] [ntext] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[overtime] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[overtime]( + [ID] [int] NOT NULL, + [이름] [nvarchar](50) NULL, + [신청일] [nchar](10) NULL, + [구분] [nvarchar](10) NULL, + [전후] [nvarchar](10) NULL, + [내용] [ntext] NULL, + [팀장] [nvarchar](10) NULL, + [팀장승인] [nvarchar](50) NULL, + [본부장] [nvarchar](10) NULL, + [본부장승인] [nvarchar](50) NULL, + [작성일] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[shortwork] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[shortwork]( + [ID] [int] NOT NULL, + [name] [nvarchar](255) NULL, + [reason] [ntext] NULL, + [timeout] [nvarchar](255) NULL, + [shorttime] [int] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[start] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[start]( + [ID] [int] NOT NULL, + [구분] [nvarchar](50) NULL, + [참조] [nvarchar](200) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[startsat] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[startsat]( + [ID] [int] NOT NULL, + [구분] [nvarchar](50) NULL, + [참조] [nvarchar](200) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[suri_count] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[suri_count]( + [ss_year] [varchar](4) NOT NULL, + [ss_month] [varchar](2) NOT NULL, + [ss_i_cnt] [int] NULL, + [ss_e_cnt] [int] NULL, + CONSTRAINT [PK_f_suri_count] PRIMARY KEY CLUSTERED +( + [ss_year] ASC, + [ss_month] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0010] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0010]( + [CORP_NO] [varchar](15) NOT NULL, + [CORP_NM] [varchar](100) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_CO0010_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0030] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0030]( + [CORP_NO] [varchar](15) NOT NULL, + [COMM_CL_CD] [varchar](5) NOT NULL, + [COMM_CL_CD_NM] [varchar](100) NULL, + [COMM_CL_CD_USE_YN] [varchar](1) NOT NULL, + [COMM_CL_CD_DSCRPT] [varchar](500) NULL, + [DSPLY_ORDR] [int] NULL, + [PROP_YN] [varchar](1) NOT NULL, + [PROP_CL1_CD] [varchar](5) NULL, + [PROP_CL2_CD] [varchar](5) NULL, + [PROP_CL3_CD] [varchar](5) NULL, + [PROP_CL4_CD] [varchar](5) NULL, + [PROP_CL5_CD] [varchar](5) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_CO0030_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [COMM_CL_CD] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0040] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0040]( + [CORP_NO] [varchar](15) NOT NULL, + [COMM_CL_CD] [varchar](5) NOT NULL, + [COMM_CD] [varchar](4) NOT NULL, + [COMM_CD_NM] [varchar](100) NULL, + [COMM_CD_USE_YN] [varchar](1) NOT NULL, + [COMM_CD_DSCRPT] [varchar](500) NULL, + [COMM_CD_DSPLY_ORDR] [int] NULL, + [COMM_CD_TYPE_VAL] [varchar](10) NULL, + [PROP2_CD] [varchar](4) NULL, + [PROP1_CD] [varchar](4) NULL, + [PROP3_CD] [varchar](4) NULL, + [PROP4_CD] [varchar](4) NULL, + [PROP5_CD] [varchar](4) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_CO0040_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [COMM_CL_CD] ASC, + [COMM_CD] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0050] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0050]( + [ATCHFILE_NO] [varchar](50) NOT NULL, + [ATCH_NO] [varchar](50) NULL, + [ATCH_FILE_PATH_NM] [varchar](200) NULL, + [ATCH_FILE_NM] [varchar](200) NULL, + [ATCH_TYPE_NM] [varchar](100) NULL, + [ORGNL_FILE_NM] [varchar](200) NULL, + [ATCH_FILE_MG] [int] NULL, + [TBL_COL_NM] [varchar](100) NULL, + [DEL_DT] [datetime] NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_CO0050_PK] PRIMARY KEY CLUSTERED +( + [ATCHFILE_NO] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0060] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0060]( + [SEQUENCE_NM] [varchar](100) NOT NULL, + [SEQUENCE_VAL] [int] NULL, + CONSTRAINT [SX_CO0060_PK] PRIMARY KEY CLUSTERED +( + [SEQUENCE_NM] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0070] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0070]( + [CORP_NO] [varchar](15) NOT NULL, + [WORK_CD] [varchar](10) NOT NULL, + [WORK_CD_TITLE_NM] [varchar](200) NULL, + [GOTOWORK_TM_NM] [varchar](10) NULL, + [GETOFFWORK_TM_NM] [varchar](10) NULL, + [PAY_DIV_NM] [varchar](100) NULL, + [REGULAR_WORK_TM] [decimal](5, 2) NULL, + [REST_TM] [decimal](5, 2) NULL, + [INCLU_WORK_NM] [varchar](100) NULL, + [INCLU_WORK_START_TM] [varchar](100) NULL, + [INCLU_WORK_END_TM] [varchar](100) NULL, + [INCLU_WORK_TM] [decimal](5, 2) NULL, + [CALC_LOCA_NM] [varchar](10) NULL, + [OT_START_TM] [varchar](100) NULL, + [NGT_ALLW_NM] [varchar](100) NULL, + [NGT_START_TM] [varchar](10) NULL, + [NGT_END_TM] [varchar](10) NULL, + [WORK_ALLW_NM] [varchar](200) NULL, + [WORK_CD_USE_YN] [varchar](1) NOT NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [WORK_CN] [varchar](4000) NULL, + [OT_BSCRT] [decimal](5, 2) NULL, + [NGT_BSCRT] [decimal](5, 2) NULL, + [APLY_WORK_DAY] [decimal](5, 2) NULL, + [YYCT_DEDU_DAY] [decimal](5, 2) NULL, + [MEAL_START_TM] [varchar](10) NULL, + [MEAL_END_TM] [varchar](10) NULL, + [HOLIDAY_YN] [varchar](10) NULL, + CONSTRAINT [SX_CO0070_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [WORK_CD] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0080] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0080]( + [CORP_NO] [varchar](15) NOT NULL, + [MENU_NO] [int] NOT NULL, + [UPPER_MENU_NO] [int] NULL, + [MENU_NM] [varchar](200) NULL, + [URL] [varchar](300) NULL, + [RM] [varchar](1000) NULL, + [MENU_PROP] [varchar](1000) NULL, + [MENU_ORDR] [int] NULL, + [MENU_USE_YN] [varchar](1) NOT NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [CONTROLLER] [varchar](100) NULL, + [MENU_LVL] [int] NULL, + CONSTRAINT [SX_CO0080_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [MENU_NO] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0090] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0090]( + [CORP_NO] [varchar](15) NOT NULL, + [MENU_NO] [int] NOT NULL, + [MENU_AUTH_CD] [varchar](4) NOT NULL, + [AUTH_MENU_PROP] [varchar](1000) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [FUNC_AUTH_CN] [varchar](100) NULL, + CONSTRAINT [SX_CO0090_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [MENU_NO] ASC, + [MENU_AUTH_CD] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0100] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0100]( + [SEQ] [numeric](38, 0) IDENTITY(1,1) NOT NULL, + [WORK_DT] [varchar](8) NOT NULL, + [WORK_ID] [nvarchar](200) NULL, + [WORK_NM] [nvarchar](100) NULL, + [WORK_TYPE] [int] NULL, + CONSTRAINT [PK_SX_CO0100] PRIMARY KEY CLUSTERED +( + [SEQ] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0110] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0110]( + [SEQ] [bigint] IDENTITY(1,1) NOT NULL, + [WORK_NM] [nvarchar](100) NULL, + CONSTRAINT [PK_SX_CO0110] PRIMARY KEY CLUSTERED +( + [SEQ] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0120] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0120]( + [SEQ] [int] IDENTITY(1,1) NOT NULL, + [B_NM] [nvarchar](30) NULL, + [B_TERM] [int] NULL, + [B_CUT] [datetime] NULL, + [B_NEXT] [datetime] NULL, + CONSTRAINT [PK_SX_CO0120] PRIMARY KEY CLUSTERED +( + [SEQ] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_CO0130] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_CO0130]( + [SEQ] [int] IDENTITY(1,1) NOT NULL, + [B_NM] [nvarchar](30) NULL, + [B_DAY] [int] NULL, + [B_DAY_VIEW] [nvarchar](10) NULL, + [B_TIME] [nvarchar](5) NULL, + CONSTRAINT [PK_SX_CO0130] PRIMARY KEY CLUSTERED +( + [SEQ] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0010] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0010]( + [CORP_NO] [varchar](15) NOT NULL, + [USR_ID] [varchar](10) NOT NULL, + [USR_SORT_ORDR] [int] NULL, + [USR_NM] [varchar](100) NOT NULL, + [PW] [varchar](100) NOT NULL, + [DUTY_CD] [varchar](4) NULL, + [BRTHDY_DATE] [varchar](8) NULL, + [USR_TELNO] [varchar](15) NULL, + [MTEL_NO] [varchar](15) NULL, + [EMAIL] [varchar](100) NULL, + [BASE_ADRS] [varchar](100) NULL, + [GUSO_ADRS] [varchar](100) NULL, + [GUSO_TELNO] [varchar](15) NULL, + [JOINCP_DATE] [varchar](8) NULL, + [SALARY] [int] NULL, + [BZ_CD] [varchar](4) NULL, + [WKDAY_WORK_CD] [varchar](4) NULL, + [WKEND_WORK_CD] [varchar](4) NULL, + [OT_AMT] [int] NULL, + [OT_CD] [varchar](4) NULL, + [FD_EXPENSES_CD] [varchar](4) NULL, + [SPOUSE_YN] [varchar](1) NOT NULL, + [SPT_FAMILY_CNT] [int] NULL, + [TEAM_CD] [varchar](4) NULL, + [MDCHIT_NM] [varchar](100) NULL, + [FINAL_SCHSP_NM] [varchar](100) NULL, + [DISML_DATE] [varchar](8) NULL, + [RETIREMENT_DATE] [varchar](8) NULL, + [RETIREMENT_RSN_CN] [varchar](500) NULL, + [MNYVALUA_ADJ_CN] [varchar](500) NULL, + [CRR_CN] [varchar](max) NULL, + [QUAL_CN] [varchar](max) NULL, + [CNTRCT_CNDT_CN] [varchar](max) NULL, + [SPCLT_ARTC_CN] [varchar](max) NULL, + [PHOTO_ATCHFILE_NO] [varchar](50) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [SPMT_EMAIL] [varchar](100) NULL, + [SPMT_MTEL_NO] [varchar](15) NULL, + [RRNO] [varchar](7) NULL, + [LOGIN_ID] [varchar](100) NULL, + [APPR_YN] [varchar](1) NULL, + CONSTRAINT [SX_GW0010_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [USR_ID] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY], + CONSTRAINT [LOGIN_ID_UNIQE] UNIQUE NONCLUSTERED +( + [LOGIN_ID] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0020] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0020]( + [CORP_NO] [varchar](15) NOT NULL, + [UNTY_BBS_CD] [varchar](4) NOT NULL, + [UNTY_BBS_SNO] [int] NOT NULL, + [BBS_TITLE_NM] [varchar](200) NULL, + [BBS_CN] [text] NULL, + [INQR_CNT] [int] NULL, + [CTUSR_ID] [varchar](10) NULL, + [ETC_ATCH_NO] [varchar](50) NULL, + [PHOTO_ATCH_NO] [varchar](50) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [CMMT_CNT] [int] NULL, + CONSTRAINT [SX_GW0020_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [UNTY_BBS_CD] ASC, + [UNTY_BBS_SNO] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0030] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0030]( + [CORP_NO] [varchar](15) NOT NULL, + [UNTY_BBS_CD] [varchar](4) NOT NULL, + [UNTY_BBS_SNO] [int] NOT NULL, + [CMMT_SNO] [int] NOT NULL, + [CMMT_CN] [varchar](max) NULL, + [CMMT_CTUSR_ID] [varchar](10) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_GW0030_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [UNTY_BBS_CD] ASC, + [UNTY_BBS_SNO] ASC, + [CMMT_SNO] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0050] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0050]( + [CORP_NO] [varchar](15) NOT NULL, + [USR_ID] [varchar](10) NOT NULL, + [WORK_PLAN_YYMMDD] [varchar](8) NOT NULL, + [PLAN_WORK_CD] [varchar](10) NULL, + [WORK_CD] [varchar](10) NULL, + [WORK_START_DT] [datetime] NULL, + [WORK_END_DT] [datetime] NULL, + [TOT_WORK_MIN] [int] NULL, + [LATE_MIN] [int] NULL, + [ABTI_YN] [varchar](1) NOT NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [INCLU_WORK_OT_MIN] [int] NULL, + [OT_WORK_MIN] [int] NULL, + [NGT_OT_MIN] [int] NULL, + [SKIPOFF_REMN_BZ_MIN] [int] NULL, + [OT_RCTN_YN] [varchar](1) NULL, + [SORT_ODR] [int] NULL, + [OUTING_MIN] [int] NULL, + [OUTING_CNT] [int] NULL, + [REAL_START_DT] [datetime] NULL, + [REAL_END_DT] [datetime] NULL, + [EL_RCTN_YN] [varchar](1) NULL, + [HOLIDAY_LATE_MIN] [int] NULL, + CONSTRAINT [SX_GW0050_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [USR_ID] ASC, + [WORK_PLAN_YYMMDD] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0060] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0060]( + [CORP_NO] [varchar](15) NOT NULL, + [USR_ID] [varchar](10) NOT NULL, + [YYVCT_YY] [varchar](4) NOT NULL, + [YYVCT_CNT] [float] NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [SORT_ODR] [int] NULL, + [REFCN] [varchar](2000) NULL, + CONSTRAINT [SX_GW0060_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [USR_ID] ASC, + [YYVCT_YY] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0080] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0080]( + [CORP_NO] [varchar](15) NOT NULL, + [APRVL_DOC_ID] [int] NOT NULL, + [LATE_SKIPOFF_ABTI_OT_CD] [varchar](4) NULL, + [LATE_SKIPOFF_ABTI_OT_DATE] [varchar](8) NULL, + [LATE_SKIPOFF_OT_DT] [datetime] NULL, + [LATE_SKIPOFF_OT_DT2] [datetime] NULL, + [LATE_SKIPOFF_OT_MIN] [int] NULL, + [NML_WORK_RCNTN_YN] [varchar](1) NOT NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [REQUEST_DT] [datetime] NULL, + [REQUEST_DT2] [datetime] NULL, + CONSTRAINT [SX_GW0080_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [APRVL_DOC_ID] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0090] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0090]( + [CORP_NO] [varchar](15) NOT NULL, + [APRVL_DOC_ID] [int] NOT NULL, + [APRVL_STUS_CD] [varchar](4) NULL, + [APRVL_KIND_CD] [varchar](4) NULL, + [APLNT_ID] [varchar](10) NULL, + [OFFER_DT] [datetime] NULL, + [CMPL_DT] [datetime] NULL, + [APLNT_CN] [varchar](1000) NULL, + [BZ_CN] [varchar](1000) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [ATCH_NO] [varchar](50) NULL, + [OFFER_CN] [varchar](4000) NULL, + [APRVL_CMPL_SNO] [int] NULL, + [FINAL_APRVL_SNO] [int] NULL, + [BZ_DEPUTY_ID] [varchar](10) NULL, + [DOC_FLAG] [varchar](2) NULL, + CONSTRAINT [SX_GW0090_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [APRVL_DOC_ID] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0100] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0100]( + [CORP_NO] [varchar](15) NOT NULL, + [APRVL_DOC_ID] [int] NOT NULL, + [APRVL_SNO] [int] NOT NULL, + [APPR_ID] [varchar](10) NULL, + [APPR_STUS_CD] [varchar](4) NULL, + [APRVL_DT] [datetime] NULL, + [APPR_CN] [varchar](1000) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + [DOC_FLAG] [varchar](2) NULL, + CONSTRAINT [SX_GW0100_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [APRVL_DOC_ID] ASC, + [APRVL_SNO] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0110] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0110]( + [CORP_NO] [varchar](15) NOT NULL, + [APRVL_DOC_ID] [int] NOT NULL, + [UPD_DATE] [varchar](8) NOT NULL, + [UPD_BF_WORK_CD] [varchar](4) NULL, + [UPD_WORK_CD] [varchar](4) NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_GW0110_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [APRVL_DOC_ID] ASC, + [UPD_DATE] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0120] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0120]( + [CORP_NO] [varchar](15) NOT NULL, + [APRVL_DOC_ID] [int] NOT NULL, + [YYVCT_DATE] [varchar](8) NULL, + [BZ_DEPUTY_ID] [varchar](10) NOT NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_GW0120_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [APRVL_DOC_ID] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0130] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0130]( + [CORP_NO] [varchar](15) NOT NULL, + [USR_ID] [varchar](10) NOT NULL, + [MENU_AUTH_CD] [varchar](4) NOT NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_GW0130_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [USR_ID] ASC, + [MENU_AUTH_CD] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[SX_GW0140] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[SX_GW0140]( + [CORP_NO] [varchar](15) NOT NULL, + [USR_ID] [varchar](10) NOT NULL, + [LV_1_APPR_YN] [varchar](1) NOT NULL, + [LV_2_APPR_YN] [varchar](1) NOT NULL, + [LV_3_APPR_YN] [varchar](1) NOT NULL, + [LV_4_APPR_YN] [varchar](1) NOT NULL, + [LV_5_APPR_YN] [varchar](1) NOT NULL, + [RGSTR_ID] [varchar](10) NOT NULL, + [RGST_DT] [datetime] NOT NULL, + [MODID] [varchar](10) NULL, + [UPD_DT] [datetime] NOT NULL, + CONSTRAINT [SX_GW0140_PK] PRIMARY KEY CLUSTERED +( + [CORP_NO] ASC, + [USR_ID] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[team] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[team]( + [ID] [int] NOT NULL, + [구분] [nvarchar](50) NULL, + [참조] [nvarchar](200) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[timecode] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[timecode]( + [ID] [int] NOT NULL, + [구분] [nvarchar](255) NULL, + [출근] [nvarchar](255) NULL, + [퇴근] [nvarchar](255) NULL, + [휴일] [int] NULL, + [제목] [nvarchar](50) NULL, + [참조] [ntext] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[users] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[users]( + [ID] [int] NOT NULL, + [순서] [int] NULL, + [u_name] [nvarchar](50) NULL, + [u_pass] [nvarchar](50) NULL, + [직책] [nvarchar](50) NULL, + [u_power] [int] NULL, + [생일] [nvarchar](50) NULL, + [집전화] [nvarchar](50) NULL, + [휴대폰] [nvarchar](50) NULL, + [이메일] [nvarchar](50) NULL, + [주소] [nvarchar](100) NULL, + [거소] [nvarchar](100) NULL, + [거소전화] [nvarchar](50) NULL, + [입사일] [nvarchar](50) NULL, + [연봉] [int] NULL, + [연가] [int] NULL, + [업무] [nvarchar](50) NULL, + [출근] [nvarchar](50) NULL, + [주말] [nvarchar](50) NULL, + [OT] [int] NULL, + [OT구분] [nvarchar](50) NULL, + [식비] [nvarchar](50) NULL, + [배우자] [nvarchar](50) NULL, + [부양가족] [int] NULL, + [팀] [nvarchar](50) NULL, + [병력] [nvarchar](50) NULL, + [학력] [nvarchar](50) NULL, + [해고일] [nvarchar](50) NULL, + [퇴직일] [nvarchar](50) NULL, + [퇴직사유] [nvarchar](250) NULL, + [금품청산] [nvarchar](250) NULL, + [경력] [ntext] NULL, + [자격] [ntext] NULL, + [계약조건] [ntext] NULL, + [참조] [ntext] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[vacation] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[vacation]( + [ID] [int] NOT NULL, + [구분] [nvarchar](20) NULL, + [예정일] [ntext] NULL, + [일수] [real] NULL, + [내용] [ntext] NULL, + [이름] [nvarchar](10) NULL, + [업무] [ntext] NULL, + [작성일] [nvarchar](50) NULL, + [대행자] [nvarchar](10) NULL, + [대행승인] [nvarchar](50) NULL, + [결재자] [nvarchar](50) NULL, + [결재] [nvarchar](50) NULL, + [결재일] [nvarchar](50) NULL, + [차감] [int] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[vacationt] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[vacationt]( + [ID] [int] NOT NULL, + [년도] [nvarchar](50) NULL, + [이름] [nvarchar](50) NULL, + [연가] [int] NULL, + [참조] [ntext] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[workchange] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[workchange]( + [ID] [int] NOT NULL, + [name] [nvarchar](50) NULL, + [date1] [nvarchar](50) NULL, + [date2] [nvarchar](50) NULL, + [workplan] [nvarchar](50) NULL, + [workchange] [nvarchar](50) NULL, + [사유] [ntext] NULL, + [업무] [ntext] NULL, + [작성일] [nvarchar](50) NULL, + [대행자] [nvarchar](50) NULL, + [대행승인] [nvarchar](50) NULL, + [승인] [nvarchar](50) NULL, + [승인시각] [nvarchar](50) NULL, + [승인자] [nvarchar](50) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[workplan] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[workplan]( + [ID] [int] NOT NULL, + [u_id] [int] NULL, + [name] [nvarchar](50) NULL, + [date] [nvarchar](50) NULL, + [주말] [nvarchar](50) NULL, + [업무] [nvarchar](50) NULL, + [d1] [nvarchar](50) NULL, + [d2] [nvarchar](50) NULL, + [d3] [nvarchar](50) NULL, + [d4] [nvarchar](50) NULL, + [d5] [nvarchar](50) NULL, + [d6] [nvarchar](50) NULL, + [d7] [nvarchar](50) NULL, + [d8] [nvarchar](50) NULL, + [d9] [nvarchar](50) NULL, + [d10] [nvarchar](50) NULL, + [d11] [nvarchar](50) NULL, + [d12] [nvarchar](50) NULL, + [d13] [nvarchar](50) NULL, + [d14] [nvarchar](50) NULL, + [d15] [nvarchar](50) NULL, + [d16] [nvarchar](50) NULL, + [d17] [nvarchar](50) NULL, + [d18] [nvarchar](50) NULL, + [d19] [nvarchar](50) NULL, + [d20] [nvarchar](50) NULL, + [d21] [nvarchar](50) NULL, + [d22] [nvarchar](50) NULL, + [d23] [nvarchar](50) NULL, + [d24] [nvarchar](50) NULL, + [d25] [nvarchar](50) NULL, + [d26] [nvarchar](50) NULL, + [d27] [nvarchar](50) NULL, + [d28] [nvarchar](50) NULL, + [d29] [nvarchar](50) NULL, + [d30] [nvarchar](50) NULL, + [d31] [nvarchar](50) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[works] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[works]( + [ID] [int] NOT NULL, + [구분] [nvarchar](50) NULL, + [참조] [nvarchar](200) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[worktime] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[worktime]( + [ID] [int] NOT NULL, + [name] [nvarchar](50) NULL, + [date] [nvarchar](50) NULL, + [timein] [nvarchar](50) NULL, + [timeout] [nvarchar](50) NULL, + [worktime] [real] NULL, + [ottime0] [real] NULL, + [ottime1] [real] NULL, + [ottime2] [real] NULL, + [ottime3] [real] NULL, + [ottime4] [real] NULL, + [tottime] [real] NULL, + [food] [real] NULL, + [otdetail] [nvarchar](200) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[환경설정] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[환경설정]( + [ID] [int] NOT NULL, + [ot] [real] NULL, + [nt] [real] NULL +) ON [PRIMARY] +GO +SET ANSI_PADDING ON +GO +/****** Object: Index [IX01_SX_GW0010] Script Date: 2026-03-30 오후 1:12:19 ******/ +CREATE UNIQUE NONCLUSTERED INDEX [IX01_SX_GW0010] ON [dbo].[SX_GW0010] +( + [CORP_NO] ASC, + [USR_NM] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +GO +ALTER TABLE [dbo].[errorMng] ADD CONSTRAINT [DF_errorMng_work_dt] DEFAULT (getdate()) FOR [work_dt] +GO +ALTER TABLE [dbo].[SX_CO0030] ADD CONSTRAINT [DF_SX_CO0030_PROP_YN] DEFAULT ('N') FOR [PROP_YN] +GO +ALTER TABLE [dbo].[SX_CO0040] ADD CONSTRAINT [DF_SX_CO0040_COMM_CD_USE_YN] DEFAULT ('N') FOR [COMM_CD_USE_YN] +GO +ALTER TABLE [dbo].[SX_CO0070] ADD CONSTRAINT [DF_SX_CO0070_WORK_CD_USE_YN] DEFAULT ('N') FOR [WORK_CD_USE_YN] +GO +ALTER TABLE [dbo].[SX_CO0070] ADD CONSTRAINT [DF_SX_CO0070_HOLIDAY_YN] DEFAULT ('N') FOR [HOLIDAY_YN] +GO +ALTER TABLE [dbo].[SX_CO0100] ADD CONSTRAINT [DF_SX_CO0100_WORK_DT] DEFAULT (CONVERT([varchar],getdate(),(112))) FOR [WORK_DT] +GO +ALTER TABLE [dbo].[SX_GW0010] ADD CONSTRAINT [DF_SX_GW0010_SPOUSE_YN] DEFAULT ('N') FOR [SPOUSE_YN] +GO +ALTER TABLE [dbo].[SX_GW0010] ADD CONSTRAINT [DF_SX_GW0010_APPR_YN] DEFAULT ('N') FOR [APPR_YN] +GO +ALTER TABLE [dbo].[SX_GW0050] ADD CONSTRAINT [DF_SX_GW0050_ABTI_YN] DEFAULT ('N') FOR [ABTI_YN] +GO +ALTER TABLE [dbo].[SX_GW0080] ADD CONSTRAINT [DF_SX_GW0080_NML_WORK_RCNTN_YN] DEFAULT ('N') FOR [NML_WORK_RCNTN_YN] +GO +ALTER TABLE [dbo].[SX_GW0140] ADD CONSTRAINT [DF_SX_GW0140_LV_1_APPR_YN] DEFAULT ('N') FOR [LV_1_APPR_YN] +GO +ALTER TABLE [dbo].[SX_GW0140] ADD CONSTRAINT [DF_SX_GW0140_LV_2_APPR_YN] DEFAULT ('N') FOR [LV_2_APPR_YN] +GO +ALTER TABLE [dbo].[SX_GW0140] ADD CONSTRAINT [DF_SX_GW0140_LV_3_APPR_YN] DEFAULT ('N') FOR [LV_3_APPR_YN] +GO +ALTER TABLE [dbo].[SX_GW0140] ADD CONSTRAINT [DF_SX_GW0140_LV_4_APPR_YN] DEFAULT ('N') FOR [LV_4_APPR_YN] +GO +ALTER TABLE [dbo].[SX_GW0140] ADD CONSTRAINT [DF_SX_GW0140_LV_5_APPR_YN] DEFAULT ('N') FOR [LV_5_APPR_YN] +GO +ALTER TABLE [dbo].[SX_CO0080] WITH CHECK ADD CONSTRAINT [SX_CO0080_SX_CO0080_FK01] FOREIGN KEY([CORP_NO], [UPPER_MENU_NO]) +REFERENCES [dbo].[SX_CO0080] ([CORP_NO], [MENU_NO]) +GO +ALTER TABLE [dbo].[SX_CO0080] CHECK CONSTRAINT [SX_CO0080_SX_CO0080_FK01] +GO +ALTER TABLE [dbo].[SX_CO0090] WITH CHECK ADD CONSTRAINT [SX_CO0090_SX_CO0080_FK01] FOREIGN KEY([CORP_NO], [MENU_NO]) +REFERENCES [dbo].[SX_CO0080] ([CORP_NO], [MENU_NO]) +GO +ALTER TABLE [dbo].[SX_CO0090] CHECK CONSTRAINT [SX_CO0090_SX_CO0080_FK01] +GO +ALTER TABLE [dbo].[SX_GW0010] WITH CHECK ADD CONSTRAINT [SX_GW0010_SX_CO0010_FK01] FOREIGN KEY([CORP_NO]) +REFERENCES [dbo].[SX_CO0010] ([CORP_NO]) +GO +ALTER TABLE [dbo].[SX_GW0010] CHECK CONSTRAINT [SX_GW0010_SX_CO0010_FK01] +GO +ALTER TABLE [dbo].[SX_GW0020] WITH CHECK ADD CONSTRAINT [SX_GW0020_SX_CO0010_FK01] FOREIGN KEY([CORP_NO]) +REFERENCES [dbo].[SX_CO0010] ([CORP_NO]) +GO +ALTER TABLE [dbo].[SX_GW0020] CHECK CONSTRAINT [SX_GW0020_SX_CO0010_FK01] +GO +ALTER TABLE [dbo].[SX_GW0030] WITH CHECK ADD CONSTRAINT [SX_GW0030_SX_GW0020_FK01] FOREIGN KEY([CORP_NO], [UNTY_BBS_CD], [UNTY_BBS_SNO]) +REFERENCES [dbo].[SX_GW0020] ([CORP_NO], [UNTY_BBS_CD], [UNTY_BBS_SNO]) +GO +ALTER TABLE [dbo].[SX_GW0030] CHECK CONSTRAINT [SX_GW0030_SX_GW0020_FK01] +GO +ALTER TABLE [dbo].[SX_GW0050] WITH CHECK ADD CONSTRAINT [SX_GW0050_SX_GW0010_FK01] FOREIGN KEY([CORP_NO], [USR_ID]) +REFERENCES [dbo].[SX_GW0010] ([CORP_NO], [USR_ID]) +GO +ALTER TABLE [dbo].[SX_GW0050] CHECK CONSTRAINT [SX_GW0050_SX_GW0010_FK01] +GO +ALTER TABLE [dbo].[SX_GW0110] WITH CHECK ADD CONSTRAINT [SX_GW0110_SX_GW0090_FK01] FOREIGN KEY([CORP_NO], [APRVL_DOC_ID]) +REFERENCES [dbo].[SX_GW0090] ([CORP_NO], [APRVL_DOC_ID]) +GO +ALTER TABLE [dbo].[SX_GW0110] CHECK CONSTRAINT [SX_GW0110_SX_GW0090_FK01] +GO +ALTER TABLE [dbo].[SX_GW0120] WITH CHECK ADD CONSTRAINT [SX_GW0120_SX_GW0090_FK01] FOREIGN KEY([CORP_NO], [APRVL_DOC_ID]) +REFERENCES [dbo].[SX_GW0090] ([CORP_NO], [APRVL_DOC_ID]) +GO +ALTER TABLE [dbo].[SX_GW0120] CHECK CONSTRAINT [SX_GW0120_SX_GW0090_FK01] +GO +ALTER TABLE [dbo].[SX_GW0130] WITH CHECK ADD CONSTRAINT [SX_GW0130_SX_GW0010_FK01] FOREIGN KEY([CORP_NO], [USR_ID]) +REFERENCES [dbo].[SX_GW0010] ([CORP_NO], [USR_ID]) +GO +ALTER TABLE [dbo].[SX_GW0130] CHECK CONSTRAINT [SX_GW0130_SX_GW0010_FK01] +GO +ALTER TABLE [dbo].[SX_GW0140] WITH CHECK ADD CONSTRAINT [SX_GW0140_SX_GW0010_FK01] FOREIGN KEY([CORP_NO], [USR_ID]) +REFERENCES [dbo].[SX_GW0010] ([CORP_NO], [USR_ID]) +GO +ALTER TABLE [dbo].[SX_GW0140] CHECK CONSTRAINT [SX_GW0140_SX_GW0010_FK01] +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_delete] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-02-02 +-- Create Description : 스카이브릿지 그룹웨어 결재문서 삭제 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_delete '1111',0, '00001' +-- exec pr_skybridge_gw_approval_delete '0000',0, '00001' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_delete] + + @CORP_NO VARCHAR(15) --회사번호CORP_NO + , @APRVL_DOC_ID INT --결재문서ID + , @USR_ID VARCHAR(10) --사용자ID + + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + DECLARE @BIZM_CNT AS INT + SET @BIZM_CNT = 0 + + + -- 업무관리자 권한이 있는지 체크 + SELECT @BIZM_CNT = COUNT('A') + FROM SX_GW0130 A + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND USR_ID = @USR_ID + AND MENU_AUTH_CD = 'BIZM' --업무관리자 + + --만약 0 이면 업무관리자 권한이 없어서 처리 불가 + IF @BIZM_CNT = 0 + BEGIN + RAISERROR('해당 ID는 업무관리자가 아닙니다. 확인해 주세요.', 16, 1) + RETURN + END + + + BEGIN TRAN + + --근무변경 삭제 (SX_GW0110) + DELETE + FROM SX_GW0110 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + --지각조퇴기록 삭제 (SX_GW0080) + DELETE + FROM SX_GW0080 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + --결재완료 삭제 (SX_GW0100) + DELETE + FROM SX_GW0100 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + --결재신청 삭제 (SX_GW0090) + DELETE + FROM SX_GW0090 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + + COMMIT; + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_delete_myDoc] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-02-02 +-- Create Description : 스카이브릿지 그룹웨어 결재문서 삭제 (본인이 작성한 문서일 경우에만) +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_delete '1111',0, '00001' +-- exec pr_skybridge_gw_approval_delete '0000',0, '00001' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_delete_myDoc] + + @CORP_NO VARCHAR(15) --회사번호CORP_NO + , @APRVL_DOC_ID INT --결재문서ID + , @USR_ID VARCHAR(10) --사용자ID + + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + DECLARE @BIZM_CNT AS INT + SET @BIZM_CNT = 0 + + + -- 업무관리자 권한이 있는지 체크 + SELECT @BIZM_CNT = COUNT('A') + FROM SX_GW0090 A + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APLNT_ID = @USR_ID + AND APRVL_DOC_ID = @APRVL_DOC_ID + AND APRVL_CMPL_SNO IS NULL + + + + --만약 0 이면 업무관리자 권한이 없어서 처리 불가 + IF @BIZM_CNT = 0 + BEGIN + RAISERROR('결재가 진행/완료 되었거나, 삭제 권한이 없습니다. 확인해 주세요.', 16, 1) + RETURN + END + + + BEGIN TRAN + + --근무변경 삭제 (SX_GW0110) + DELETE + FROM SX_GW0110 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + --지각조퇴기록 삭제 (SX_GW0080) + DELETE + FROM SX_GW0080 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + --결재완료 삭제 (SX_GW0100) + DELETE + FROM SX_GW0100 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + --결재신청 삭제 (SX_GW0090) + DELETE + FROM SX_GW0090 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND APRVL_DOC_ID = @APRVL_DOC_ID + + + COMMIT; + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_document_group_count] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 내가 올린 결재 문서 개수 +-- Update date : +-- Update Description : + +--exec pr_skybridge_gw_approval_document_group_count '1111', '00001' +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_document_group_count] + + @CORP_NO VARCHAR(15) --회사코드 + , @USR_ID VARCHAR(20) --사용자 + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + SELECT + 1 AS SORT + ,'☆내가 올린 결재 문서' AS STUS_NM + ,'' AS DOC_CNT + + UNION ALL + SELECT + 2 AS SORT + , ' - 작성중' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0090 + WHERE 1=1 + AND APRVL_STUS_CD IS NOT NULL + AND DATEPART(YYYY, RGST_DT) = DATEPART(YYYY, GETDATE()) + AND CORP_NO = @CORP_NO + AND APLNT_ID = @USR_ID + AND APRVL_STUS_CD = '0001' + + UNION ALL + SELECT + 3 AS SORT + , ' - 미결재' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0090 + WHERE 1=1 + AND APRVL_STUS_CD IS NOT NULL + AND DATEPART(YYYY, RGST_DT) = DATEPART(YYYY, GETDATE()) + AND CORP_NO = @CORP_NO + AND APLNT_ID = @USR_ID + AND APRVL_STUS_CD = '0002' + + UNION ALL + SELECT + 4 AS SORT + , ' - 승인' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0090 + WHERE 1=1 + AND APRVL_STUS_CD IS NOT NULL + AND DATEPART(YYYY, RGST_DT) = DATEPART(YYYY, GETDATE()) + AND CORP_NO = @CORP_NO + AND APLNT_ID = @USR_ID + AND APRVL_STUS_CD = '0003' + AND ISNULL(DOC_FLAG, '') != 'A' +/* + UNION ALL + SELECT + 5 AS SORT + , ' - 반려' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0090 A + LEFT OUTER JOIN SX_GW0100 B + ON A.CORP_NO = B.CORP_NO AND A.APRVL_DOC_ID = B.APRVL_DOC_ID AND APRVL_SNO = 1 + WHERE 1=1 + AND A.APRVL_STUS_CD IS NOT NULL + AND DATEPART(YYYY, A.RGST_DT) = DATEPART(YYYY, GETDATE()) + AND A.CORP_NO = @CORP_NO + AND A.APLNT_ID = @USR_ID + AND A.APRVL_STUS_CD = '0004' + AND ISNULL(B.DOC_FLAG, '') = 'A' + AND ISNULL(A.DOC_FLAG, '') != 'A' +*/ + + UNION ALL + SELECT + 5 AS SORT + , ' - 반려' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0090 A + LEFT OUTER JOIN SX_GW0100 B + ON A.CORP_NO = B.CORP_NO AND A.APRVL_DOC_ID = B.APRVL_DOC_ID AND APRVL_SNO = 1 + WHERE 1=1 + AND A.APRVL_STUS_CD IS NOT NULL + AND DATEPART(YYYY, A.RGST_DT) = DATEPART(YYYY, GETDATE()) + AND A.CORP_NO = @CORP_NO + AND A.APLNT_ID = @USR_ID + AND A.APRVL_STUS_CD = '0004' + AND (B.APPR_STUS_CD = '0004' OR (B.APPR_STUS_CD != '0004' AND ISNULL(B.DOC_FLAG, '') = 'A')) + AND ISNULL(A.DOC_FLAG, '') != 'A' + /* + AND B.APPR_STUS_CD = '0004' + AND ISNULL(A.DOC_FLAG, '') != 'A' + */ + + UNION ALL + SELECT + 6 AS SORT + ,'☆내가 받은 결재 문서' AS STUS_NM + ,'' AS DOC_CNT + + UNION ALL + SELECT + 7 AS SORT + , ' - 미결재' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0100 A + LEFT OUTER JOIN SX_GW0090 B + ON A.CORP_NO = B.CORP_NO AND A.APRVL_DOC_ID = B.APRVL_DOC_ID + WHERE 1=1 + AND B.APRVL_DOC_ID IS NOT NULL + AND DATEPART(YYYY, A.RGST_DT) = DATEPART(YYYY, GETDATE()) + AND A.CORP_NO = @CORP_NO + AND A.APPR_ID = @USR_ID + AND ISNULL(A.APPR_STUS_CD, '') = '0002' + + UNION ALL + SELECT + 8 AS SORT + , ' - 미확인' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0100 A + LEFT OUTER JOIN SX_GW0090 B + ON A.CORP_NO = B.CORP_NO AND A.APRVL_DOC_ID = B.APRVL_DOC_ID + WHERE 1=1 + AND B.APRVL_DOC_ID IS NOT NULL + AND DATEPART(YYYY, A.RGST_DT) = DATEPART(YYYY, GETDATE()) + AND A.CORP_NO = @CORP_NO + AND A.APPR_ID = @USR_ID + AND A.APRVL_SNO = 1 + AND ISNULL(A.APPR_STUS_CD, '') = '0003' + AND ISNULL(B.APRVL_STUS_CD, '') = '0004' + AND ISNULL(A.DOC_FLAG, '') != 'A' + + UNION ALL + SELECT + 9 AS SORT + , ' - 승인' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0100 A + LEFT OUTER JOIN SX_GW0090 B + ON A.CORP_NO = B.CORP_NO AND A.APRVL_DOC_ID = B.APRVL_DOC_ID + WHERE 1=1 + AND B.APRVL_DOC_ID IS NOT NULL + AND DATEPART(YYYY, A.RGST_DT) = DATEPART(YYYY, GETDATE()) + AND A.CORP_NO = @CORP_NO + AND A.APPR_ID = @USR_ID + AND A.APRVL_SNO = 1 + AND ISNULL(A.APPR_STUS_CD, '') = '0003' + AND ISNULL(B.APRVL_STUS_CD, '') = '0002' + + UNION ALL + SELECT + 10 AS SORT + , ' - 반려' AS STUS_NM + , '('+ CONVERT(VARCHAR, COUNT('A')) +')' AS DOC_CNT + FROM SX_GW0100 A + LEFT OUTER JOIN SX_GW0090 B + ON A.CORP_NO = B.CORP_NO AND A.APRVL_DOC_ID = B.APRVL_DOC_ID + LEFT OUTER JOIN SX_GW0100 C + ON A.CORP_NO = C.CORP_NO AND A.APRVL_DOC_ID = C.APRVL_DOC_ID AND A.APRVL_SNO != C.APRVL_SNO + WHERE 1=1 + AND B.APRVL_DOC_ID IS NOT NULL + AND DATEPART(YYYY, A.RGST_DT) = DATEPART(YYYY, GETDATE()) + AND A.CORP_NO = @CORP_NO + AND A.APPR_ID = @USR_ID + AND ISNULL(B.APRVL_STUS_CD, '') = '0004' + AND( + (A.APRVL_SNO = 1 AND ISNULL(B.DOC_FLAG, '') != 'A' AND A.DOC_FLAG = 'A') + OR + (A.APRVL_SNO = 2 AND ISNULL(C.DOC_FLAG, '') != 'A' AND ISNULL(A.APPR_STUS_CD, '') = '0004') + ) + + ORDER BY SORT ASC + + + + +END --PROCEDURE END +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_document_list_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 결재문서 결재 리스트 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_document_list_select '1111','00001','','','','','','',1,20000,20000, '20070101', '20171231', '' --1페이지 +-- exec pr_skybridge_gw_approval_document_list_select '1111','00001','','','','','','',2,20,20, '20070101', '20171231', '' --2페이지 +-- exec pr_skybridge_gw_approval_document_list_select '1111','10212','','','','','','',1,20000,20000,'20070101', '20171231', '' --1페이지 + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_document_list_select] + + @P0 VARCHAR(15) --CORP_NO + , @P1 VARCHAR(10) --APPR_ID + , @P2 VARCHAR(20) -- + , @P3 VARCHAR(4) --APPR_STUS_CD + , @P4 VARCHAR(20) -- + , @P5 VARCHAR(4) --APRVL_STUS_CD + , @P6 VARCHAR(20) -- + , @P7 VARCHAR(4) --APRVL_KIND_CD + , @P8 INT -- 조회 페이지 번호 + , @P9 INT -- 한행에 조회되는 행수 + , @P10 INT -- 한행에 조회되는 행수 + , @P11 VARCHAR(20) -- 검색 시작일 + , @P12 VARCHAR(20) -- 검색 종료일 + , @P13 VARCHAR(4) -- 진행상태 +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + -- 신규 작성 + SELECT + A.APRVL_SNO, + CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + A.APPR_STUS_CD, + + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + SUBSTRING(CONVERT(VARCHAR, A.OFFER_DT, 120),1,10) AS OFFER_DT, + CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + + + ISNULL(A.APRVL_CMPL_SNO,0) AS APRVL_CMPL_SNO, + ISNULL(A.FINAL_APRVL_SNO,0) AS FINAL_APRVL_SNO, + + -- ■ 스카이브릿지 결재문서 결재 상세 내역 정리 + --- 1.시간외 : 신청내용, 업무내용, OT일자 + --- 2.지각 : 신청내용, 업무내용, 정상근무인정 요청, 지각일자(X), 지각일시 + --- 3.조퇴 : 신청내용, 업무내용, 정상근무인정 요청, 조퇴일자(X), 조퇴일시 + --- 4.출퇴근미기록 : 신청내용, 업무내용, 정상근무인정 요청, 결근일자 + --- 5.연차등 : 신청내용, 업무내용, 대행자, 연차 (날자, 연차등) + --- 6.근무변경 : 신청내용, 업무내용, 대행자, 근무변경(근무일, 변경전, 변경후) + --- 7.기안 : 신청내용, 업무내용 + + A.APLNT_CN , --신청자 내용 + A.BZ_CN, --업무내용 + + --근무코드 : 변경후 근무코드 조회 없으면 변경전 근무코드 조회 + CASE WHEN APRVL_KIND_CD = '0001' THEN ( SELECT CASE WHEN ISNULL(WORK_CD, '') = '' THEN PLAN_WORK_CD ELSE WORK_CD END AS WORK_CD + FROM SX_GW0050 + WHERE CORP_NO = @P0 + AND USR_ID = APLNT_ID + AND WORK_PLAN_YYMMDD = REPLACE(LATE_SKIPOFF_ABTI_OT_DATE, '-', '') + ) + + WHEN APRVL_KIND_CD = '0002' THEN ( SELECT CASE WHEN ISNULL(WORK_CD, '') = '' THEN PLAN_WORK_CD ELSE WORK_CD END AS WORK_CD + FROM SX_GW0050 + WHERE CORP_NO = @P0 + AND USR_ID = APLNT_ID + AND WORK_PLAN_YYMMDD = REPLACE(SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120),1,10), '-', '') + ) + + WHEN APRVL_KIND_CD = '0003' THEN ( SELECT CASE WHEN ISNULL(WORK_CD, '') = '' THEN PLAN_WORK_CD ELSE WORK_CD END AS WORK_CD + FROM SX_GW0050 + WHERE CORP_NO = @P0 + AND USR_ID = APLNT_ID + AND WORK_PLAN_YYMMDD = REPLACE(SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120),1,10), '-', '') + ) + + WHEN APRVL_KIND_CD = '0004' THEN ( SELECT CASE WHEN ISNULL(WORK_CD, '') = '' THEN PLAN_WORK_CD ELSE WORK_CD END AS WORK_CD + FROM SX_GW0050 + WHERE CORP_NO = @P0 + AND USR_ID = APLNT_ID + AND WORK_PLAN_YYMMDD = REPLACE(SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_ABTI_OT_DATE, 120),1,10), '-', '') + ) + + WHEN APRVL_KIND_CD = '0005' THEN ( SELECT CASE WHEN ISNULL(WORK_CD, '') = '' THEN PLAN_WORK_CD ELSE WORK_CD END AS WORK_CD + FROM SX_GW0050 + WHERE CORP_NO = @P0 + AND USR_ID = APLNT_ID + AND WORK_PLAN_YYMMDD = REPLACE(SUBSTRING(A.UPD_DATE_YC,1,10), '-', '') + ) + + WHEN APRVL_KIND_CD = '0006' THEN UPD_BF_WORK_CD + + WHEN APRVL_KIND_CD = '0007' THEN '' + + ELSE '' + END AS WORK_CD , + + + --상세1 + CASE WHEN APRVL_KIND_CD = '0001' THEN 'OT : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0002' THEN '실제 : ' + SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120),1,16) + WHEN APRVL_KIND_CD = '0003' THEN CASE WHEN A.NML_WORK_RCNTN_YN = 'Y'THEN '조기퇴근' ELSE '' END -- '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0004' THEN '출퇴근미기록 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0005' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0006' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_01 , --상세내역 01 + + + --상세2 + CASE WHEN APRVL_KIND_CD = '0001' THEN ( SELECT CASE WHEN ISNULL(WORK_START_DT, '') = '' + THEN SUBSTRING(CONVERT(VARCHAR, REAL_START_DT, 120),12,5) + ELSE SUBSTRING(CONVERT(VARCHAR, WORK_START_DT, 120),12,5) END + + ' ~ ' + + CASE WHEN ISNULL(WORK_END_DT, '') = '' + THEN SUBSTRING(CONVERT(VARCHAR, REAL_END_DT, 120),12,5) + ELSE SUBSTRING(CONVERT(VARCHAR, WORK_END_DT, 120),12,5) END + FROM SX_GW0050 + WHERE CORP_NO = @P0 + AND USR_ID = APLNT_ID + AND WORK_PLAN_YYMMDD = REPLACE(LATE_SKIPOFF_ABTI_OT_DATE, '-', '') + ) + WHEN APRVL_KIND_CD = '0002' THEN '승인 : ' + SUBSTRING(CONVERT(VARCHAR, A.REQUEST_DT, 120),1,16) + WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120),1,16) + WHEN APRVL_KIND_CD = '0004' THEN SUBSTRING(CONVERT(VARCHAR, REQUEST_DT, 120),12,5) + ' ~ ' + SUBSTRING(CONVERT(VARCHAR, REQUEST_DT2, 120),12,5) + WHEN APRVL_KIND_CD = '0005' THEN '연차 : ' + UPD_DATE_YC + ' ' + FREE_CNT + WHEN APRVL_KIND_CD = '0006' THEN '변경 : ' + UPD_DATE_CHANGE + '(' + UPD_BF_WORK_CD + '-->' + UPD_WORK_CD + ')' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_02 , -- 상세내역02 + + + --상세3 + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' + WHEN APRVL_KIND_CD = '0003' THEN '' + WHEN APRVL_KIND_CD = '0004' THEN '' + WHEN APRVL_KIND_CD = '0005' THEN '' + WHEN APRVL_KIND_CD = '0006' THEN '' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_03 , --상세내역 03 + + --A.OFFER_CN, --신청내용 + --A.UPD_DT, --변경일시 + + A.APLNT_ID, + A.APLNT_NM, + A.LATE_SKIPOFF_ABTI_OT_DATE + FROM ( + SELECT A.CORP_NO, + A.APRVL_SNO, + A.APRVL_DT, + A.APPR_STUS_CD, + B.APRVL_DOC_ID, + B.APRVL_STUS_CD, + B.APRVL_KIND_CD, + B.OFFER_DT, + B.CMPL_DT, + B.APRVL_CMPL_SNO, + B.FINAL_APRVL_SNO, + B.APLNT_ID, + + U1.USR_NM AS APLNT_NM, + + U2.LATE_SKIPOFF_ABTI_OT_DATE, + U2.NML_WORK_RCNTN_YN, + U2.LATE_SKIPOFF_OT_DT, + U2.REQUEST_DT, --승인출근요청일시 + U2.REQUEST_DT2, --승인퇴근요청일시 + + U3.UPD_DATE_YC, + U3.FREE_CNT, + + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_DATE_CHANGE, + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + B.APLNT_CN , + B.BZ_CN, + B.OFFER_CN, + B.UPD_DT + + --FROM SX_GW0090 B + -- LEFT OUTER JOIN + -- ( SELECT * FROM SX_GW0100 + -- WHERE 1=1 + -- AND CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + CONVERT(VARCHAR, APRVL_SNO) IN + -- (SELECT CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + MAX(CONVERT(VARCHAR, APRVL_SNO)) + -- FROM SX_GW0100 + -- WHERE CORP_NO = @P0 + -- GROUP BY CORP_NO, APRVL_DOC_ID + --) + + + FROM SX_GW0100 A LEFT JOIN + SX_GW0090 B + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = A.CORP_NO + AND U2.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + , CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + ) U3 + ON(1=1 + AND U3.CORP_NO = A.CORP_NO + AND U3.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + + WHERE 1=1 + AND B.APRVL_DOC_ID IS NOT NULL + AND A.CORP_NO = @P0 + AND (A.APPR_ID = @P1) + AND (NULLIF(@P2, '') IS NULL OR A.APPR_STUS_CD = @P3) + AND (NULLIF(@P4, '') IS NULL OR B.APRVL_STUS_CD = @P5) + AND (NULLIF(@P6, '') IS NULL OR B.APRVL_KIND_CD = @P7) + + AND (CONVERT(VARCHAR, B.UPD_DT, 112) BETWEEN REPLACE(@P11, '-', '') AND REPLACE(@P12, '-', '')) + + /* 추가 조건 시작 */ + AND ( + (NULLIF(@P13, '') IS NULL) + OR (NULLIF(@P13, '0001') IS NULL AND ISNULL(A.APPR_STUS_CD, '') = '0002') -- 미결재 + OR (NULLIF(@P13, '0002') IS NULL AND (A.APRVL_SNO = '1' AND B.APRVL_STUS_CD = '0004' AND A.APPR_STUS_CD = '0003' AND ISNULL(A.DOC_FLAG, '') != 'A')) -- 미확인 + OR (NULLIF(@P13, '0003') IS NULL AND (A.APRVL_SNO = '1' AND B.APRVL_STUS_CD = '0002' AND A.APPR_STUS_CD = '0003')) -- 승인 + + OR (NULLIF(@P13, '0004') IS NULL AND B.APRVL_STUS_CD = '0004' AND ( + (A.APPR_ID = @P1 AND A.APRVL_SNO = '2' AND ISNULL(A.APPR_STUS_CD, '') = '0004' AND ISNULL((SELECT DOC_FLAG FROM SX_GW0100 WHERE CORP_NO = A.CORP_NO AND APRVL_SNO = '1' AND APRVL_DOC_ID = A.APRVL_DOC_ID), '') != 'A' ) + OR + (A.APPR_ID = @P1 AND A.APRVL_SNO = '1' AND ISNULL(A.DOC_FLAG, '') != 'A') + ) + ) --반려 + ) + /* 추가 조건 끝 */ + + ORDER BY + B.OFFER_DT DESC, + APRVL_SNO DESC + + + OFFSET ((@P8 - 1) * @P9) ROWS + FETCH NEXT @P10 ROWS ONLY + + ) A + + + + + --기존에 사용한 문장 BACKUP + --SELECT + -- A.APRVL_SNO, + -- CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + -- A.APPR_STUS_CD, + + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + -- CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + + -- A.APLNT_ID, + -- A.APLNT_NM + -- FROM ( + + + + --SELECT + -- A.APRVL_SNO, + -- A.APRVL_DT, + -- A.APPR_STUS_CD, + -- B.APRVL_DOC_ID, + -- B.APRVL_STUS_CD, + -- B.APRVL_KIND_CD, + -- B.OFFER_DT, + -- B.CMPL_DT, + -- B.APRVL_CMPL_SNO, + -- B.FINAL_APRVL_SNO, + -- B.APLNT_ID, + -- U1.USR_NM AS APLNT_NM + + + -- FROM SX_GW0100 A + -- JOIN SX_GW0090 B + -- ON(1=1 + -- AND B.CORP_NO = A.CORP_NO + -- AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + -- ) + -- LEFT JOIN SX_GW0010 U1 + -- ON(1=1 + -- AND U1.CORP_NO = B.CORP_NO + -- AND U1.USR_ID = B.APLNT_ID + -- ) + -- WHERE 1=1 + -- AND A.CORP_NO = @P0 + -- AND A.APPR_ID = @P1 + -- AND (NULLIF(@P2, '''') IS NULL OR A.APPR_STUS_CD = @P3) + + -- AND (NULLIF(@P4, '''') IS NULL OR B.APRVL_STUS_CD = @P5) + -- AND (NULLIF(@P6, '''') IS NULL OR B.APRVL_KIND_CD = @P7) + + + -- ORDER BY + -- B.OFFER_DT DESC, + -- APRVL_SNO DESC + + + --OFFSET ((@P8 - 1) * @P9) ROWS + --FETCH NEXT @P10 ROWS ONLY + + -- ) A + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_document_list_select_20200109_bak] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 결재문서 결재 리스트 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_document_list_select '1111','00001','','','','','','',1,20000,20000 --1페이지 +-- exec pr_skybridge_gw_approval_document_list_select '1111','00001','','','','','','',2,20,20 --2페이지 +-- exec pr_skybridge_gw_approval_document_list_select '1111','10212','','','','','','',1,20000,20000 --1페이지 +-- EXEC pr_skybridge_gw_approval_document_list_select '1111', '10305', '', '', '', '', '', '', 1, 10000, 10000 --김은중 + +-- ======================================================================================= +create PROCEDURE [dbo].[pr_skybridge_gw_approval_document_list_select_20200109_bak] + + @P0 VARCHAR(15) --CORP_NO + , @P1 VARCHAR(10) --APPR_ID + , @P2 VARCHAR(20) -- + , @P3 VARCHAR(4) --APPR_STUS_CD + , @P4 VARCHAR(20) -- + , @P5 VARCHAR(4) --APRVL_STUS_CD + , @P6 VARCHAR(20) -- + , @P7 VARCHAR(4) --APRVL_KIND_CD + , @P8 INT -- 조회 페이지 번호 + , @P9 INT -- 한행에 조회되는 행수 + , @P10 INT -- 한행에 조회되는 행수 + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + -- 신규 작성 + SELECT + A.APRVL_SNO, + CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + A.APPR_STUS_CD, + + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + + + ISNULL(A.APRVL_CMPL_SNO,0) AS APRVL_CMPL_SNO, + ISNULL(A.FINAL_APRVL_SNO,0) AS FINAL_APRVL_SNO, + + -- ■ 스카이브릿지 결재문서 결재 상세 내역 정리 + --- 1.시간외 : 신청내용, 업무내용, OT일자 + --- 2.지각 : 신청내용, 업무내용, 정상근무인정 요청, 지각일자(X), 지각일시 + --- 3.조퇴 : 신청내용, 업무내용, 정상근무인정 요청, 조퇴일자(X), 조퇴일시 + --- 4.결근 : 신청내용, 업무내용, 정상근무인정 요청, 결근일자 + --- 5.연차등 : 신청내용, 업무내용, 대행자, 연차 (날자, 연차등) + --- 6.근무변경 : 신청내용, 업무내용, 대행자, 근무변경(근무일, 변경전, 변경후) + --- 7.기안 : 신청내용, 업무내용 + + A.APLNT_CN , --신청자 내용 + A.BZ_CN, --업무내용 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0003' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0004' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0005' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0006' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0007' THEN '' + WHEN APRVL_KIND_CD = '0008' THEN '' + ELSE '' + END AS DETAIL_01 , --상세내역 01 + + CASE WHEN APRVL_KIND_CD = '0001' THEN 'OT : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0002' THEN '지각 : ' + (CASE WHEN ISNULL(A.REQUEST_DT,'') = '' THEN SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120), 1, 16) ELSE SUBSTRING(CONVERT(VARCHAR, A.REQUEST_DT, 120), 1, 16) END) + WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + (CASE WHEN ISNULL(A.REQUEST_DT2,'') = '' THEN SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120), 1, 16) ELSE SUBSTRING(CONVERT(VARCHAR, A.REQUEST_DT2, 120), 1, 16) END) + WHEN APRVL_KIND_CD = '0004' THEN '결근 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + ' ' + ISNULL(SUBSTRING(CONVERT(VARCHAR, REQUEST_DT, 108), 1, 5), '') + '~' + ISNULL(SUBSTRING(CONVERT(VARCHAR, REQUEST_DT2, 108), 1, 5), '') + WHEN APRVL_KIND_CD = '0005' THEN '연차 : ' + UPD_DATE_YC + ' ' + FREE_CNT + WHEN APRVL_KIND_CD = '0006' THEN '변경 : ' + UPD_DATE_CHANGE + '(' + UPD_BF_WORK_CD + '-->' + UPD_WORK_CD + ')' + WHEN APRVL_KIND_CD = '0007' THEN '' + WHEN APRVL_KIND_CD = '0008' THEN '외출 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 23) + ' (' + SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 108),1,5) + '~' + SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT2, 108),1,5) + ')' + ELSE '' + END AS DETAIL_02 , -- 상세내역02 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' + WHEN APRVL_KIND_CD = '0003' THEN '' + WHEN APRVL_KIND_CD = '0004' THEN '' + WHEN APRVL_KIND_CD = '0005' THEN '' + WHEN APRVL_KIND_CD = '0006' THEN '' + WHEN APRVL_KIND_CD = '0007' THEN '' + WHEN APRVL_KIND_CD = '0008' THEN '' + ELSE '' + END AS DETAIL_03 , --상세내역 03 + + --A.OFFER_CN, --신청내용 + --A.UPD_DT, --변경일시 + + A.APLNT_ID, + A.APLNT_NM + FROM ( + SELECT A.CORP_NO, + A.APRVL_SNO, + A.APRVL_DT, + A.APPR_STUS_CD, + B.APRVL_DOC_ID, + B.APRVL_STUS_CD, + B.APRVL_KIND_CD, + B.OFFER_DT, + B.CMPL_DT, + B.APRVL_CMPL_SNO, + B.FINAL_APRVL_SNO, + B.APLNT_ID, + U1.USR_NM AS APLNT_NM, + U2.LATE_SKIPOFF_ABTI_OT_DATE, + U2.NML_WORK_RCNTN_YN, + U2.LATE_SKIPOFF_OT_DT, + U2.LATE_SKIPOFF_OT_DT2, + + U2.REQUEST_DT, --승인출근시각 + U2.REQUEST_DT2, --승인퇴근시각 + + U3.UPD_DATE_YC, + U3.FREE_CNT, + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_DATE_CHANGE, + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + B.APLNT_CN , + B.BZ_CN, + B.OFFER_CN, + B.UPD_DT + + --FROM SX_GW0090 B + -- LEFT OUTER JOIN + -- ( SELECT * FROM SX_GW0100 + -- WHERE 1=1 + -- AND CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + CONVERT(VARCHAR, APRVL_SNO) IN + -- (SELECT CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + MAX(CONVERT(VARCHAR, APRVL_SNO)) + -- FROM SX_GW0100 + -- WHERE CORP_NO = @P0 + -- GROUP BY CORP_NO, APRVL_DOC_ID + --) + + + FROM SX_GW0100 A LEFT JOIN + SX_GW0090 B + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = A.CORP_NO + AND U2.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + -- , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + --, MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + TC.PLAN_WORK_CD + '-->' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + + --CASE WHEN ISNULL(TC.PLAN_WORK_CD, '') = '' THEN '( --> )' + CASE WHEN ISNULL(TC.PLAN_WORK_CD, '') = '' THEN '( -->' + UPD_WORK_CD + ')' + ELSE '(' + TC.PLAN_WORK_CD + '-->' + UPD_WORK_CD + ')' + END ) AS UPD_DATE_YC --연차 + + --, CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + , SUBSTRING(ISNULL(CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)), '0.00'),1,3) + '일' AS FREE_CNT --연차 일수 + + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + LEFT JOIN SX_GW0050 TC ON TC.CORP_NO = TA.CORP_NO AND TC.USR_ID = TA.RGSTR_ID AND TC.WORK_PLAN_YYMMDD = TA.UPD_DATE + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + ) U3 + ON(1=1 + AND U3.CORP_NO = A.CORP_NO + AND U3.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + + WHERE 1=1 + AND A.CORP_NO = @P0 + AND (A.APPR_ID = @P1 OR A.RGSTR_ID = @P1) + AND (NULLIF(@P2, '') IS NULL OR A.APPR_STUS_CD = @P3) + + AND (NULLIF(@P4, '') IS NULL OR B.APRVL_STUS_CD = @P5) + AND (NULLIF(@P6, '') IS NULL OR B.APRVL_KIND_CD = @P7) + + ORDER BY + B.OFFER_DT DESC, + APRVL_SNO DESC + + + OFFSET ((@P8 - 1) * @P9) ROWS + FETCH NEXT @P10 ROWS ONLY + + ) A + + + + + --기존에 사용한 문장 BACKUP + --SELECT + -- A.APRVL_SNO, + -- CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + -- A.APPR_STUS_CD, + + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + -- CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + + -- A.APLNT_ID, + -- A.APLNT_NM + -- FROM ( + + + + --SELECT + -- A.APRVL_SNO, + -- A.APRVL_DT, + -- A.APPR_STUS_CD, + -- B.APRVL_DOC_ID, + -- B.APRVL_STUS_CD, + -- B.APRVL_KIND_CD, + -- B.OFFER_DT, + -- B.CMPL_DT, + -- B.APRVL_CMPL_SNO, + -- B.FINAL_APRVL_SNO, + -- B.APLNT_ID, + -- U1.USR_NM AS APLNT_NM + + + -- FROM SX_GW0100 A + -- JOIN SX_GW0090 B + -- ON(1=1 + -- AND B.CORP_NO = A.CORP_NO + -- AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + -- ) + -- LEFT JOIN SX_GW0010 U1 + -- ON(1=1 + -- AND U1.CORP_NO = B.CORP_NO + -- AND U1.USR_ID = B.APLNT_ID + -- ) + -- WHERE 1=1 + -- AND A.CORP_NO = @P0 + -- AND A.APPR_ID = @P1 + -- AND (NULLIF(@P2, '''') IS NULL OR A.APPR_STUS_CD = @P3) + + -- AND (NULLIF(@P4, '''') IS NULL OR B.APRVL_STUS_CD = @P5) + -- AND (NULLIF(@P6, '''') IS NULL OR B.APRVL_KIND_CD = @P7) + + + -- ORDER BY + -- B.OFFER_DT DESC, + -- APRVL_SNO DESC + + + --OFFSET ((@P8 - 1) * @P9) ROWS + --FETCH NEXT @P10 ROWS ONLY + + -- ) A + + + +END --PROCEDURE END +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_document_list_select_count] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 결재문서 결재 리스트 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_document_list_select '1111','00001','','','','','','',1,20000,20000, '20070101', '20171231', '' --1페이지 +-- exec pr_skybridge_gw_approval_document_list_select '1111','00001','','','','','','',2,20,20, '20070101', '20171231', '' --2페이지 +-- exec pr_skybridge_gw_approval_document_list_select '1111','10212','','','','','','',1,20000,20000,'20070101', '20171231', '' --1페이지 + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_document_list_select_count] + + @P0 VARCHAR(15) --CORP_NO + , @P1 VARCHAR(10) --APPR_ID + , @P2 VARCHAR(20) -- + , @P3 VARCHAR(4) --APPR_STUS_CD + , @P4 VARCHAR(20) -- + , @P5 VARCHAR(4) --APRVL_STUS_CD + , @P6 VARCHAR(20) -- + , @P7 VARCHAR(4) --APRVL_KIND_CD + , @P8 INT -- 조회 페이지 번호 + , @P9 INT -- 한행에 조회되는 행수 + , @P10 INT -- 한행에 조회되는 행수 + , @P11 VARCHAR(20) -- 검색 시작일 + , @P12 VARCHAR(20) -- 검색 종료일 + , @P13 VARCHAR(4) -- 진행상태 +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + -- 신규 작성 + SELECT + COUNT('A') AS TOT_CNT + FROM ( + SELECT A.CORP_NO, + A.APRVL_SNO, + A.APRVL_DT, + A.APPR_STUS_CD, + B.APRVL_DOC_ID, + B.APRVL_STUS_CD, + B.APRVL_KIND_CD, + B.OFFER_DT, + B.CMPL_DT, + B.APRVL_CMPL_SNO, + B.FINAL_APRVL_SNO, + B.APLNT_ID, + + U1.USR_NM AS APLNT_NM, + + U2.LATE_SKIPOFF_ABTI_OT_DATE, + U2.NML_WORK_RCNTN_YN, + U2.LATE_SKIPOFF_OT_DT, + U2.REQUEST_DT, --승인출근요청일시 + U2.REQUEST_DT2, --승인퇴근요청일시 + + U3.UPD_DATE_YC, + U3.FREE_CNT, + + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_DATE_CHANGE, + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + B.APLNT_CN , + B.BZ_CN, + B.OFFER_CN, + B.UPD_DT + + FROM SX_GW0100 A LEFT JOIN + SX_GW0090 B + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = A.CORP_NO + AND U2.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + , CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + ) U3 + ON(1=1 + AND U3.CORP_NO = A.CORP_NO + AND U3.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + + WHERE 1=1 + AND B.APRVL_DOC_ID IS NOT NULL + AND A.CORP_NO = @P0 + AND A.APPR_ID = @P1 + AND (NULLIF(@P2, '') IS NULL OR A.APPR_STUS_CD = @P3) + + AND (NULLIF(@P4, '') IS NULL OR B.APRVL_STUS_CD = @P5) + AND (NULLIF(@P6, '') IS NULL OR B.APRVL_KIND_CD = @P7) + + AND (CONVERT(VARCHAR, B.UPD_DT, 112) BETWEEN @P11 AND @P12) + + /* 추가 조건 시작 */ + AND ( + (NULLIF(@P13, '') IS NULL) + OR (NULLIF(@P13, '0001') IS NULL AND ISNULL(A.APPR_STUS_CD, '') = '0002') -- 미결재 + OR (NULLIF(@P13, '0002') IS NULL AND (A.APRVL_SNO = '1' AND B.APRVL_STUS_CD = '0004' AND A.APPR_STUS_CD = '0003' AND ISNULL(A.DOC_FLAG, '') != 'A')) -- 미확인 + OR (NULLIF(@P13, '0003') IS NULL AND (A.APRVL_SNO = '1' AND B.APRVL_STUS_CD = '0002' AND A.APPR_STUS_CD = '0003')) -- 승인 + + OR (NULLIF(@P13, '0004') IS NULL AND B.APRVL_STUS_CD = '0004' AND ( + (A.APRVL_SNO = '2' AND ISNULL(A.APPR_STUS_CD, '') = '0004' AND ISNULL((SELECT DOC_FLAG FROM SX_GW0100 WHERE CORP_NO = A.CORP_NO AND APRVL_SNO = '1' AND APRVL_DOC_ID = A.APRVL_DOC_ID), '') != 'A' ) + OR + (A.APRVL_SNO = '1' AND A.DOC_FLAG = 'A' AND ISNULL(B.DOC_FLAG, '') = '') + ) + ) --반려 + ) + /* 추가 조건 끝 */ + + + + ) A + + + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_manager_list_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 결재문서 결재 리스트 조회(관리자용) +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_manager_list_select '','','','','','',NULL,'1111','0003','0003','','','20161215','20170215',1,20000,20000 +-- exec pr_skybridge_gw_approval_manager_list_select '','','','','','',NULL,'1111','','','','','20161215','20170215',1,20000,20000 +-- exec pr_skybridge_gw_approval_manager_list_select '10429','10429','','','','',NULL,'1111','0003','0003','','','20161217','20170217',1,2000,2000 + +-- exec pr_skybridge_gw_approval_manager_list_select '지희','지희','','','','',NULL,'1111','0003','0003','','','20161217','20170217',1,2000,2000 + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_manager_list_select] + + @P0 nvarchar(4000) + , @P1 nvarchar(4000) + , @P2 nvarchar(4000) + , @P3 nvarchar(4000) + , @P4 nvarchar(4000) + , @P5 nvarchar(4000) + , @P6 varchar(8000) + , @P7 nvarchar(4000) + , @P8 nvarchar(4000) + , @P9 nvarchar(4000) + , @P10 nvarchar(4000) + , @P11 nvarchar(4000) + , @P12 nvarchar(4000) + , @P13 nvarchar(4000) + , @P14 int + , @P15 int + , @P16 int + + + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + ---- 신규 작성 + SELECT + A.APRVL_SNO, + CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + A.APPR_STUS_CD, + + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + + + ISNULL(A.APRVL_CMPL_SNO,0) AS APRVL_CMPL_SNO, + ISNULL(A.FINAL_APRVL_SNO,0) AS FINAL_APRVL_SNO, + + -- ■ 스카이브릿지 결재문서 결재 상세 내역 정리 + --- 1.시간외 : 신청내용, 업무내용, OT일자 + --- 2.지각 : 신청내용, 업무내용, 정상근무인정 요청, 지각일자(X), 지각일시 + --- 3.조퇴 : 신청내용, 업무내용, 정상근무인정 요청, 조퇴일자(X), 조퇴일시 + --- 4.결근 : 신청내용, 업무내용, 정상근무인정 요청, 결근일자 + --- 5.연차등 : 신청내용, 업무내용, 대행자, 연차 (날자, 연차등) + --- 6.근무변경 : 신청내용, 업무내용, 대행자, 근무변경(근무일, 변경전, 변경후) + --- 7.기안 : 신청내용, 업무내용 + + A.APLNT_CN , --신청자 내용 + A.BZ_CN, --업무내용 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0003' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0004' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0005' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0006' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_01 , --상세내역 01 + + CASE WHEN APRVL_KIND_CD = '0001' THEN 'OT : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + + --WHEN APRVL_KIND_CD = '0002' THEN '지각 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + --WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + --WHEN APRVL_KIND_CD = '0004' THEN '결근 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + + WHEN APRVL_KIND_CD = '0002' THEN '지각 : ' + (CASE WHEN ISNULL(A.REQUEST_DT,'') = '' THEN SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120), 1, 16) ELSE SUBSTRING(CONVERT(VARCHAR, A.REQUEST_DT, 120), 1, 16) END) + WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + (CASE WHEN ISNULL(A.REQUEST_DT2,'') = '' THEN SUBSTRING(CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120), 1, 16) ELSE SUBSTRING(CONVERT(VARCHAR, A.REQUEST_DT2, 120), 1, 16) END) + WHEN APRVL_KIND_CD = '0004' THEN '결근 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + ' ' + ISNULL(SUBSTRING(CONVERT(VARCHAR, REQUEST_DT, 108), 1, 5), '') + '~' + ISNULL(SUBSTRING(CONVERT(VARCHAR, REQUEST_DT2, 108), 1, 5), '') + + WHEN APRVL_KIND_CD = '0005' THEN '연차 : ' + UPD_DATE_YC + ' ' + FREE_CNT + WHEN APRVL_KIND_CD = '0006' THEN '변경 : ' + UPD_DATE_CHANGE + '(' + UPD_BF_WORK_CD + '-->' + UPD_WORK_CD + ')' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_02 , -- 상세내역02 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' + WHEN APRVL_KIND_CD = '0003' THEN '' + WHEN APRVL_KIND_CD = '0004' THEN '' + WHEN APRVL_KIND_CD = '0005' THEN '' + WHEN APRVL_KIND_CD = '0006' THEN '' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_03 , --상세내역 03 + + + + --A.OFFER_CN, --신청내용 + --A.UPD_DT, --변경일시 + + A.APLNT_ID, + A.APLNT_NM, + A.APLNT_TEAM_CD, + A.APLNT_DUTY_CD + + FROM ( + SELECT A.CORP_NO, + A.APRVL_SNO, + A.APRVL_DT, + A.APPR_STUS_CD, + B.APRVL_DOC_ID, + B.APRVL_STUS_CD, + B.APRVL_KIND_CD, + B.OFFER_DT, + B.CMPL_DT, + B.APRVL_CMPL_SNO, + B.FINAL_APRVL_SNO, + + B.APLNT_ID, + U1.USR_NM AS APLNT_NM, + U1.TEAM_CD AS APLNT_TEAM_CD, + U1.DUTY_CD AS APLNT_DUTY_CD, + + U2.LATE_SKIPOFF_ABTI_OT_DATE, + U2.NML_WORK_RCNTN_YN, + U2.LATE_SKIPOFF_OT_DT, + + U2.REQUEST_DT, --승인출근시각 + U2.REQUEST_DT2, --승인퇴근시각 + + + U3.UPD_DATE_YC, + U3.FREE_CNT, + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_DATE_CHANGE, + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + B.APLNT_CN , + B.BZ_CN, + B.OFFER_CN, + B.UPD_DT + + FROM SX_GW0090 B LEFT JOIN + ( SELECT * FROM SX_GW0100 + WHERE 1=1 + AND CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + CONVERT(VARCHAR, APRVL_SNO) IN + (SELECT CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + MAX(CONVERT(VARCHAR, APRVL_SNO)) + FROM SX_GW0100 + WHERE CORP_NO = @P7 + GROUP BY CORP_NO, APRVL_DOC_ID ) + + ) A + + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + --AND (NULLIF(@P0, '') IS NULL OR U1.USR_ID LIKE '%' + @P1 + '%') --사용자ID로 조회 + AND (NULLIF(@P0, '') IS NULL OR U1.USR_NM LIKE '%' + @P1 + '%') --사용자명으로 조회 + AND (NULLIF(@P2, '') IS NULL OR U1.TEAM_CD = @P3) + AND (NULLIF(@P4, '') IS NULL OR U1.DUTY_CD = @P5) + AND (@P6 = 'Y' OR NULLIF(U1.RETIREMENT_DATE, '') IS NULL) + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = B.CORP_NO + AND U2.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + , CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + + + + ) U3 + ON(1=1 + AND U3.CORP_NO = B.CORP_NO + AND U3.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + + + WHERE 1=1 + AND B.CORP_NO = @P7 + AND (NULLIF(@P8, '') IS NULL OR B.APRVL_STUS_CD = @P9) + AND (NULLIF(@P10, '') IS NULL OR B.APRVL_KIND_CD = @P11) + AND CONVERT(VARCHAR, ISNULL(B.OFFER_DT, B.RGST_DT), 112) BETWEEN @P12 AND @P13 + + + ORDER BY + ISNULL(B.OFFER_DT, B.RGST_DT) DESC + + + OFFSET ((@P14 - 1) * @P15) ROWS + FETCH NEXT @P16 ROWS ONLY + + + + ) A + + + + + --기존에 사용한 문장 BACKUP + --SELECT + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + -- CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + -- A.APLNT_ID, + -- A.APLNT_NM, + -- A.APLNT_TEAM_CD, + -- A.APLNT_DUTY_CD + -- FROM ( + -- SELECT + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- A.OFFER_DT, + -- A.CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + -- A.APLNT_ID, + -- U.USR_NM AS APLNT_NM, + -- U.TEAM_CD AS APLNT_TEAM_CD, + -- U.DUTY_CD AS APLNT_DUTY_CD + + + -- FROM SX_GW0090 A + -- JOIN SX_GW0010 U + -- ON(1=1 + -- AND U.CORP_NO = A.CORP_NO + -- AND U.USR_ID = A.APLNT_ID + -- AND (NULLIF(@P0, '') IS NULL OR U.USR_ID = @P1) + -- AND (NULLIF(@P2, '') IS NULL OR U.TEAM_CD = @P3) + -- AND (NULLIF(@P4, '') IS NULL OR U.DUTY_CD = @P5) + -- AND (@P6 = 'Y' OR NULLIF(U.RETIREMENT_DATE, '') IS NULL) + -- ) + -- WHERE 1=1 + -- AND A.CORP_NO = @P7 + -- AND (NULLIF(@P8, '') IS NULL OR A.APRVL_STUS_CD = @P9) + -- AND (NULLIF(@P10, '') IS NULL OR A.APRVL_KIND_CD = @P11) + -- AND CONVERT(VARCHAR, ISNULL(A.OFFER_DT, A.RGST_DT), 112) BETWEEN @P12 AND @P13 + + -- ORDER BY + -- ISNULL(A.OFFER_DT, A.RGST_DT) DESC + + + -- OFFSET ((@P14 - 1) * @P15) ROWS + -- FETCH NEXT @P16 ROWS ONLY + + -- ) A + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_multi_delete] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-02-02 +-- Create Description : 스카이브릿지 그룹웨어 결재문서 삭제- 다수건 삭제 +-- Update date : +-- Update Description : + +/* + +DECLARE @ParamTable ApprovalParamTableType + +insert into @ParamTable values ('1111', 338, '00001') +insert into @ParamTable values ('1111', 341, '00001') +insert into @ParamTable values ('1111', 349, '00001') + +exec pr_skybridge_gw_approval_multi_delete @ParamTable + + +*/ + + +-- ======================================================================================= + +/* Create a table type. -- 테이블 타입 선언 +CREATE TYPE ApprovalParamTableType AS TABLE +( CORP_NO VARCHAR(15) --회사번호CORP_NO + , APRVL_DOC_ID INT --결재문서ID + , USR_ID VARCHAR(10) --사용자ID + ); +GO +*/ + +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_multi_delete] + + @ParamTable_1 ApprovalParamTableType READONLY + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + --PARAMETER로 넘김 DATA를 FETCH하면서 담을 변수 지정 + DECLARE @CORP_NO VARCHAR(15) --회사번호CORP_NO + DECLARE @APRVL_DOC_ID INT --결재문서ID + DECLARE @USR_ID VARCHAR(10) --사용자ID + + + + DECLARE Cursor_1 CURSOR FOR + + SELECT CORP_NO + , APRVL_DOC_ID + , USR_ID + FROM @ParamTable_1 + + OPEN Cursor_1; + + FETCH NEXT FROM Cursor_1 INTO @CORP_NO, @APRVL_DOC_ID, @USR_ID; + + WHILE @@FETCH_STATUS = 0 --FETCH가 성공하면 + + BEGIN + + exec pr_skybridge_gw_approval_delete @CORP_NO, @APRVL_DOC_ID, @USR_ID + + FETCH NEXT FROM Cursor_1 INTO @CORP_NO, @APRVL_DOC_ID, @USR_ID; + + END; + + CLOSE Cursor_1; + + DEALLOCATE Cursor_1; + + + +END --PROCEDURE END + + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_request_list_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 결재 신청 리스트 조회 (개별) +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_request_list_select '1111','00001','','','','','20161215','20170215',1,20000,20000 --1페이지 +-- exec pr_skybridge_gw_approval_request_list_select '1111','00001','','','','','20160101','20170215',1,20000,20000 --1페이지 + + + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_request_list_select] + + @P0 VARCHAR(4000) --CORP_NO + , @P1 VARCHAR(4000) --APPR_ID + , @P2 VARCHAR(4000) -- + , @P3 VARCHAR(4000) -- + , @P4 VARCHAR(4000) -- + , @P5 VARCHAR(4000) -- + , @P6 VARCHAR(4000) -- + , @P7 VARCHAR(4000) --APRVL_KIND_CD + , @P8 INT -- 조회 페이지 번호 + , @P9 INT -- 한행에 조회되는 행수 + , @P10 INT -- 한행에 조회되는 행수 + , @P11 VARCHAR(4000) -- + , @P12 VARCHAR(4000) -- + , @P13 VARCHAR(4000) -- + , @P14 VARCHAR(4000) --APRVL_KIND_CD + , @P15 VARCHAR(10) -- PROCESS_CD + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + + /* 0001 : 작성중 */ --AND (B.APRVL_STUS_CD = '0001') + /* 0002 : 미결재 */ --AND (B.APRVL_STUS_CD = '0002') + /* 0003 : 승인 */ --AND (B.APRVL_STUS_CD = '0003' AND ISNULL(B.DOC_FLAG, '') != 'A') + /* 0004 : 반려 */ --AND (B.APRVL_STUS_CD = '0004' AND ISNULL(B.DOC_FLAG, '') != 'A' AND ISNULL(CH1.APPR_STUS_CD, '') = '0004' ) + + + -- 2017.07.12 + SELECT + A.APRVL_SNO, + CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + A.APPR_STUS_CD, + + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + + + ISNULL(A.APRVL_CMPL_SNO,0) AS APRVL_CMPL_SNO, + ISNULL(A.FINAL_APRVL_SNO,0) AS FINAL_APRVL_SNO, + + -- ■ 스카이브릿지 결재문서 결재 상세 내역 정리 + --- 1.시간외 : 신청내용, 업무내용, OT일자 + --- 2.지각 : 신청내용, 업무내용, 정상근무인정 요청, 지각일자(X), 지각일시 + --- 3.조퇴 : 신청내용, 업무내용, 정상근무인정 요청, 조퇴일자(X), 조퇴일시 + --- 4.결근 : 신청내용, 업무내용, 정상근무인정 요청, 결근일자 + --- 5.연차등 : 신청내용, 업무내용, 대행자, 연차 (날자, 연차등) + --- 6.근무변경 : 신청내용, 업무내용, 대행자, 근무변경(근무일, 변경전, 변경후) + --- 7.기안 : 신청내용, 업무내용 + + A.APLNT_CN , --신청자 내용 + A.BZ_CN, --업무내용 + + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' --'인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0003' THEN CASE WHEN A.NML_WORK_RCNTN_YN = 'Y'THEN '조기퇴근' ELSE '' END -- '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0004' THEN '' --'인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0005' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0006' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_01 , --상세내역 01 + + + CASE WHEN APRVL_KIND_CD = '0001' THEN 'OT : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0002' THEN '지각 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0004' THEN '결근 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0005' THEN '연차 : ' + UPD_DATE_YC + ' ' + FREE_CNT + WHEN APRVL_KIND_CD = '0006' THEN '변경 : ' + UPD_DATE_CHANGE + '(' + UPD_BF_WORK_CD + '-->' + UPD_WORK_CD + ')' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_02 , -- 상세내역02 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' + WHEN APRVL_KIND_CD = '0003' THEN '' + WHEN APRVL_KIND_CD = '0004' THEN '' + WHEN APRVL_KIND_CD = '0005' THEN '' + WHEN APRVL_KIND_CD = '0006' THEN '' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_03 , --상세내역 03 + + + --A.OFFER_CN, --신청내용 + --A.UPD_DT, --변경일시 + + A.APLNT_ID, + A.APLNT_NM, + PROCESS_CD + FROM ( + SELECT A.CORP_NO + , A.APRVL_SNO + , A.APRVL_DT + , A.APPR_STUS_CD + , A.RGST_DT + , B.APRVL_DOC_ID + , B.APRVL_STUS_CD + , B.APRVL_KIND_CD + , B.OFFER_DT + , B.CMPL_DT + , B.APRVL_CMPL_SNO + , B.FINAL_APRVL_SNO + , B.APLNT_ID + , U1.USR_NM AS APLNT_NM + , U2.LATE_SKIPOFF_ABTI_OT_DATE + , U2.NML_WORK_RCNTN_YN + , U2.LATE_SKIPOFF_OT_DT + , U3.UPD_DATE_YC + , U3.FREE_CNT + , U4.USR_NM AS BZ_DEPUTY_NM + + , U3.UPD_DATE_CHANGE + , U3.UPD_BF_WORK_CD + , U3.UPD_WORK_CD + + , B.APLNT_CN + , B.BZ_CN + , B.OFFER_CN + , B.UPD_DT + + , CASE WHEN B.APRVL_STUS_CD = '0001' THEN '0001' + + WHEN B.APRVL_STUS_CD = '0002' THEN '0002' + WHEN B.APRVL_STUS_CD = '0003' AND ISNULL(B.DOC_FLAG, '') != 'A' THEN '0003' + --WHEN B.APRVL_STUS_CD = '0004' AND ISNULL(B.DOC_FLAG, '') != 'A' AND ISNULL(CH1.APPR_STUS_CD, '') = '0004' THEN '0004' + WHEN B.APRVL_STUS_CD = '0004' AND ISNULL(B.DOC_FLAG, '') != 'A' AND (ISNULL(CH1.APPR_STUS_CD, '') = '0004' OR ISNULL(CH1.DOC_FLAG, '') = 'A') THEN '0004' + ELSE '' + END AS PROCESS_CD + + FROM SX_GW0090 B + LEFT OUTER JOIN + ( SELECT * FROM SX_GW0100 + WHERE 1=1 + AND CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + CONVERT(VARCHAR, APRVL_SNO) IN + (SELECT CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + MAX(CONVERT(VARCHAR, APRVL_SNO)) + FROM SX_GW0100 + WHERE CORP_NO = @P0 + GROUP BY CORP_NO, APRVL_DOC_ID ) + + ) A + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = B.CORP_NO + AND U2.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + , CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + ) U3 + ON(1=1 + AND U3.CORP_NO = B.CORP_NO + AND U3.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + LEFT OUTER JOIN SX_GW0100 CH1 + ON B.CORP_NO = CH1.CORP_NO AND B.APRVL_DOC_ID = CH1.APRVL_DOC_ID AND CH1.APRVL_SNO = 1 + + + WHERE 1=1 + AND B.CORP_NO = @P0 + AND B.APLNT_ID = @P1 + AND (NULLIF(@P2, '') IS NULL OR B.APRVL_STUS_CD = @P3) + AND + ( + (NULLIF(@P4, '') IS NULL OR (LEN(@P11) = 4 AND B.APRVL_KIND_CD = @P5 ) ) + OR + (NULLIF(@P12, '') IS NULL OR (LEN(@P13) > 4 AND B.APRVL_KIND_CD IN(/*'0001', */'0002', '0003', '0008', '0004') ) ) + ) + AND CONVERT(VARCHAR, ISNULL(B.OFFER_DT, B.RGST_DT), 112) BETWEEN @P6 AND @P7 + + /* + ORDER BY + ISNULL(B.OFFER_DT, A.RGST_DT) DESC + + + OFFSET ((@P8 - 1) * @P9) ROWS + FETCH NEXT @P10 ROWS ONLY + */ + + ) A + WHERE 1=1 + AND (NULLIF(@P15, '') IS NULL OR A.PROCESS_CD = @P15) + + ORDER BY + ISNULL(A.OFFER_DT, A.RGST_DT) DESC + + + OFFSET ((@P8 - 1) * @P9) ROWS + FETCH NEXT @P10 ROWS ONLY + + + /* 백업_v2 + SELECT + A.APRVL_SNO, + CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + A.APPR_STUS_CD, + + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + + + ISNULL(A.APRVL_CMPL_SNO,0) AS APRVL_CMPL_SNO, + ISNULL(A.FINAL_APRVL_SNO,0) AS FINAL_APRVL_SNO, + + -- ■ 스카이브릿지 결재문서 결재 상세 내역 정리 + --- 1.시간외 : 신청내용, 업무내용, OT일자 + --- 2.지각 : 신청내용, 업무내용, 정상근무인정 요청, 지각일자(X), 지각일시 + --- 3.조퇴 : 신청내용, 업무내용, 정상근무인정 요청, 조퇴일자(X), 조퇴일시 + --- 4.결근 : 신청내용, 업무내용, 정상근무인정 요청, 결근일자 + --- 5.연차등 : 신청내용, 업무내용, 대행자, 연차 (날자, 연차등) + --- 6.근무변경 : 신청내용, 업무내용, 대행자, 근무변경(근무일, 변경전, 변경후) + --- 7.기안 : 신청내용, 업무내용 + + A.APLNT_CN , --신청자 내용 + A.BZ_CN, --업무내용 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0003' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0004' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0005' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0006' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_01 , --상세내역 01 + + CASE WHEN APRVL_KIND_CD = '0001' THEN 'OT : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0002' THEN '지각 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0004' THEN '결근 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0005' THEN '연차 : ' + UPD_DATE_YC + ' ' + FREE_CNT + WHEN APRVL_KIND_CD = '0006' THEN '변경 : ' + UPD_DATE_CHANGE + '(' + UPD_BF_WORK_CD + '-->' + UPD_WORK_CD + ')' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_02 , -- 상세내역02 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' + WHEN APRVL_KIND_CD = '0003' THEN '' + WHEN APRVL_KIND_CD = '0004' THEN '' + WHEN APRVL_KIND_CD = '0005' THEN '' + WHEN APRVL_KIND_CD = '0006' THEN '' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_03 , --상세내역 03 + + + --A.OFFER_CN, --신청내용 + --A.UPD_DT, --변경일시 + + A.APLNT_ID, + A.APLNT_NM + FROM ( + SELECT A.CORP_NO, + A.APRVL_SNO, + A.APRVL_DT, + A.APPR_STUS_CD, + B.APRVL_DOC_ID, + B.APRVL_STUS_CD, + B.APRVL_KIND_CD, + B.OFFER_DT, + B.CMPL_DT, + B.APRVL_CMPL_SNO, + B.FINAL_APRVL_SNO, + B.APLNT_ID, + U1.USR_NM AS APLNT_NM, + U2.LATE_SKIPOFF_ABTI_OT_DATE, + U2.NML_WORK_RCNTN_YN, + U2.LATE_SKIPOFF_OT_DT, + U3.UPD_DATE_YC, + U3.FREE_CNT, + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_DATE_CHANGE, + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + B.APLNT_CN , + B.BZ_CN, + B.OFFER_CN, + B.UPD_DT + + FROM SX_GW0090 B + LEFT OUTER JOIN + ( SELECT * FROM SX_GW0100 + WHERE 1=1 + AND CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + CONVERT(VARCHAR, APRVL_SNO) IN + (SELECT CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + MAX(CONVERT(VARCHAR, APRVL_SNO)) + FROM SX_GW0100 + WHERE CORP_NO = @P0 + GROUP BY CORP_NO, APRVL_DOC_ID ) + + ) A + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = B.CORP_NO + AND U2.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + , CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + ) U3 + ON(1=1 + AND U3.CORP_NO = B.CORP_NO + AND U3.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + + WHERE 1=1 + AND B.CORP_NO = @P0 + AND B.APLNT_ID = @P1 + AND (NULLIF(@P2, '') IS NULL OR B.APRVL_STUS_CD = @P3) + AND + ( + (NULLIF(@P4, '') IS NULL OR (LEN(@P11) = 4 AND B.APRVL_KIND_CD = @P5 ) ) + OR + (NULLIF(@P12, '') IS NULL OR (LEN(@P13) > 4 AND B.APRVL_KIND_CD IN('0002', '0003', '0008', '0004') ) ) + ) + AND CONVERT(VARCHAR, ISNULL(B.OFFER_DT, B.RGST_DT), 112) BETWEEN @P6 AND @P7 + + ORDER BY + ISNULL(B.OFFER_DT, A.RGST_DT) DESC + + + OFFSET ((@P8 - 1) * @P9) ROWS + FETCH NEXT @P10 ROWS ONLY + + + ) A + */ + + + + --기존에 사용한 문장 BACKUP + --declare @p1 int + --set @p1=1492 + --exec sp_prepexec @p1 output + + --@P0 nvarchar(4000) + --,@P1 nvarchar(4000) + --,@P2 nvarchar(4000) + --,@P3 nvarchar(4000) + --,@P4 nvarchar(4000) + --,@P5 nvarchar(4000) + --,@P6 nvarchar(4000) + --,@P7 nvarchar(4000) + --,@P8 int + --,@P9 int + --,@P10 int + + --SELECT + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + -- CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + -- (SELECT TOP 1 USR_NM FROM SX_GW0010 WHERE USR_ID = A.APLNT_ID) AS APLNT_NM + -- FROM ( + + -- SELECT + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- A.OFFER_DT, + -- A.CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + -- A.APLNT_ID + + + -- FROM SX_GW0090 A + -- WHERE 1=1 + -- AND A.CORP_NO = @P0 + -- AND A.APLNT_ID = @P1 + -- AND (NULLIF(@P2, '') IS NULL OR A.APRVL_STUS_CD = @P3) + -- AND (NULLIF(@P4, '') IS NULL OR A.APRVL_KIND_CD = @P5) + -- AND CONVERT(VARCHAR, ISNULL(A.OFFER_DT, A.RGST_DT), 112) BETWEEN @P6 AND @P7 + + + -- ORDER BY + -- ISNULL(A.OFFER_DT, A.RGST_DT) DESC + + + --OFFSET ((@P8 - 1) * @P9) ROWS + --FETCH NEXT @P10 ROWS ONLY + + -- ) A + + + --'1111','00001','','','','','20161215','20170215',1,20,20 + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_approval_request_list_select_count] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : Byun +-- Create date : 2017-07-12 +-- Create Description : 스카이브릿지 그룹웨어 결재 신청 리스트 개수 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_approval_request_list_select '1111','00001','','','','','20161215','20170215',1,20000,20000 --1페이지 +-- exec pr_skybridge_gw_approval_request_list_select '1111','00001','','','','','20160101','20170215',1,20000,20000 --1페이지 + + + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_approval_request_list_select_count] + + @P0 VARCHAR(4000) --CORP_NO + , @P1 VARCHAR(4000) --APPR_ID + , @P2 VARCHAR(4000) -- + , @P3 VARCHAR(4000) -- + , @P4 VARCHAR(4000) -- + , @P5 VARCHAR(4000) -- + , @P6 VARCHAR(4000) -- + , @P7 VARCHAR(4000) --APRVL_KIND_CD + , @P8 INT -- 조회 페이지 번호 + , @P9 INT -- 한행에 조회되는 행수 + , @P10 INT -- 한행에 조회되는 행수 + , @P11 VARCHAR(4000) -- + , @P12 VARCHAR(4000) -- + , @P13 VARCHAR(4000) -- + , @P14 VARCHAR(4000) --APRVL_KIND_CD + , @P15 VARCHAR(10) -- PROCESS_CD + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + + /* 0001 : 작성중 */ --AND (B.APRVL_STUS_CD = '0001') + /* 0002 : 미결재 */ --AND (B.APRVL_STUS_CD = '0002') + /* 0003 : 승인 */ --AND (B.APRVL_STUS_CD = '0003' AND ISNULL(B.DOC_FLAG, '') != 'A') + /* 0004 : 반려 */ --AND (B.APRVL_STUS_CD = '0004' AND ISNULL(B.DOC_FLAG, '') != 'A' AND ISNULL(CH1.DOC_FLAG, '') = 'A' ) + + + -- 2017.07.12 + SELECT COUNT('A') AS TOT_CNT + FROM( + SELECT + A.APRVL_SNO, + CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + A.APPR_STUS_CD, + + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + + + ISNULL(A.APRVL_CMPL_SNO,0) AS APRVL_CMPL_SNO, + ISNULL(A.FINAL_APRVL_SNO,0) AS FINAL_APRVL_SNO, + + -- ■ 스카이브릿지 결재문서 결재 상세 내역 정리 + --- 1.시간외 : 신청내용, 업무내용, OT일자 + --- 2.지각 : 신청내용, 업무내용, 정상근무인정 요청, 지각일자(X), 지각일시 + --- 3.조퇴 : 신청내용, 업무내용, 정상근무인정 요청, 조퇴일자(X), 조퇴일시 + --- 4.결근 : 신청내용, 업무내용, 정상근무인정 요청, 결근일자 + --- 5.연차등 : 신청내용, 업무내용, 대행자, 연차 (날자, 연차등) + --- 6.근무변경 : 신청내용, 업무내용, 대행자, 근무변경(근무일, 변경전, 변경후) + --- 7.기안 : 신청내용, 업무내용 + + A.APLNT_CN , --신청자 내용 + A.BZ_CN, --업무내용 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0003' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0004' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0005' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0006' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_01 , --상세내역 01 + + CASE WHEN APRVL_KIND_CD = '0001' THEN 'OT : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0002' THEN '지각 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0004' THEN '결근 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0005' THEN '연차 : ' + UPD_DATE_YC + ' ' + FREE_CNT + WHEN APRVL_KIND_CD = '0006' THEN '변경 : ' + UPD_DATE_CHANGE + '(' + UPD_BF_WORK_CD + '-->' + UPD_WORK_CD + ')' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_02 , -- 상세내역02 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' + WHEN APRVL_KIND_CD = '0003' THEN '' + WHEN APRVL_KIND_CD = '0004' THEN '' + WHEN APRVL_KIND_CD = '0005' THEN '' + WHEN APRVL_KIND_CD = '0006' THEN '' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_03 , --상세내역 03 + + + --A.OFFER_CN, --신청내용 + --A.UPD_DT, --변경일시 + + A.APLNT_ID, + A.APLNT_NM, + PROCESS_CD + FROM ( + SELECT A.CORP_NO + , A.APRVL_SNO + , A.APRVL_DT + , A.APPR_STUS_CD + , B.APRVL_DOC_ID + , B.APRVL_STUS_CD + , B.APRVL_KIND_CD + , B.OFFER_DT + , B.CMPL_DT + , B.APRVL_CMPL_SNO + , B.FINAL_APRVL_SNO + , B.APLNT_ID + , U1.USR_NM AS APLNT_NM + , U2.LATE_SKIPOFF_ABTI_OT_DATE + , U2.NML_WORK_RCNTN_YN + , U2.LATE_SKIPOFF_OT_DT + , U3.UPD_DATE_YC + , U3.FREE_CNT + , U4.USR_NM AS BZ_DEPUTY_NM + + , U3.UPD_DATE_CHANGE + , U3.UPD_BF_WORK_CD + , U3.UPD_WORK_CD + + , B.APLNT_CN + , B.BZ_CN + , B.OFFER_CN + , B.UPD_DT + + , CASE WHEN B.APRVL_STUS_CD = '0001' THEN '0001' + + WHEN B.APRVL_STUS_CD = '0002' THEN '0002' + WHEN B.APRVL_STUS_CD = '0003' AND ISNULL(B.DOC_FLAG, '') != 'A' THEN '0003' + WHEN B.APRVL_STUS_CD = '0004' AND ISNULL(B.DOC_FLAG, '') != 'A' AND ISNULL(CH1.DOC_FLAG, '') = 'A' THEN '0004' + ELSE '' + END AS PROCESS_CD + + FROM SX_GW0090 B + LEFT OUTER JOIN + ( SELECT * FROM SX_GW0100 + WHERE 1=1 + AND CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + CONVERT(VARCHAR, APRVL_SNO) IN + (SELECT CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + MAX(CONVERT(VARCHAR, APRVL_SNO)) + FROM SX_GW0100 + WHERE CORP_NO = @P0 + GROUP BY CORP_NO, APRVL_DOC_ID ) + + ) A + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = B.CORP_NO + AND U2.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + , CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + ) U3 + ON(1=1 + AND U3.CORP_NO = B.CORP_NO + AND U3.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + LEFT OUTER JOIN SX_GW0100 CH1 + ON B.CORP_NO = CH1.CORP_NO AND B.APRVL_DOC_ID = CH1.APRVL_DOC_ID AND CH1.APRVL_SNO = 1 + + + WHERE 1=1 + AND B.CORP_NO = @P0 + AND B.APLNT_ID = @P1 + AND (NULLIF(@P2, '') IS NULL OR B.APRVL_STUS_CD = @P3) + AND + ( + (NULLIF(@P4, '') IS NULL OR (LEN(@P11) = 4 AND B.APRVL_KIND_CD = @P5 ) ) + OR + (NULLIF(@P12, '') IS NULL OR (LEN(@P13) > 4 AND B.APRVL_KIND_CD IN(/*'0001', */'0002', '0003', '0008', '0004') ) ) + ) + AND CONVERT(VARCHAR, ISNULL(B.OFFER_DT, B.RGST_DT), 112) BETWEEN @P6 AND @P7 + + ) A + WHERE 1=1 + AND (NULLIF(@P15, '') IS NULL OR A.PROCESS_CD = @P15) + ) AS TOT + + + /* 백업_v2 + SELECT + A.APRVL_SNO, + CONVERT(VARCHAR, A.APRVL_DT, 120) AS APRVL_DT, + A.APPR_STUS_CD, + + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + + + ISNULL(A.APRVL_CMPL_SNO,0) AS APRVL_CMPL_SNO, + ISNULL(A.FINAL_APRVL_SNO,0) AS FINAL_APRVL_SNO, + + -- ■ 스카이브릿지 결재문서 결재 상세 내역 정리 + --- 1.시간외 : 신청내용, 업무내용, OT일자 + --- 2.지각 : 신청내용, 업무내용, 정상근무인정 요청, 지각일자(X), 지각일시 + --- 3.조퇴 : 신청내용, 업무내용, 정상근무인정 요청, 조퇴일자(X), 조퇴일시 + --- 4.결근 : 신청내용, 업무내용, 정상근무인정 요청, 결근일자 + --- 5.연차등 : 신청내용, 업무내용, 대행자, 연차 (날자, 연차등) + --- 6.근무변경 : 신청내용, 업무내용, 대행자, 근무변경(근무일, 변경전, 변경후) + --- 7.기안 : 신청내용, 업무내용 + + A.APLNT_CN , --신청자 내용 + A.BZ_CN, --업무내용 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0003' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0004' THEN '인정 : ' + A.NML_WORK_RCNTN_YN + WHEN APRVL_KIND_CD = '0005' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0006' THEN '대행자 : ' + A.BZ_DEPUTY_NM + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_01 , --상세내역 01 + + CASE WHEN APRVL_KIND_CD = '0001' THEN 'OT : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0002' THEN '지각 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0003' THEN '조퇴 : ' + CONVERT(VARCHAR, A.LATE_SKIPOFF_OT_DT, 120) + WHEN APRVL_KIND_CD = '0004' THEN '결근 : ' + CONVERT(VARCHAR, CONVERT(DATETIME, A.LATE_SKIPOFF_ABTI_OT_DATE), 23) + WHEN APRVL_KIND_CD = '0005' THEN '연차 : ' + UPD_DATE_YC + ' ' + FREE_CNT + WHEN APRVL_KIND_CD = '0006' THEN '변경 : ' + UPD_DATE_CHANGE + '(' + UPD_BF_WORK_CD + '-->' + UPD_WORK_CD + ')' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_02 , -- 상세내역02 + + CASE WHEN APRVL_KIND_CD = '0001' THEN '' + WHEN APRVL_KIND_CD = '0002' THEN '' + WHEN APRVL_KIND_CD = '0003' THEN '' + WHEN APRVL_KIND_CD = '0004' THEN '' + WHEN APRVL_KIND_CD = '0005' THEN '' + WHEN APRVL_KIND_CD = '0006' THEN '' + WHEN APRVL_KIND_CD = '0007' THEN '' + ELSE '' + END AS DETAIL_03 , --상세내역 03 + + + --A.OFFER_CN, --신청내용 + --A.UPD_DT, --변경일시 + + A.APLNT_ID, + A.APLNT_NM + FROM ( + SELECT A.CORP_NO, + A.APRVL_SNO, + A.APRVL_DT, + A.APPR_STUS_CD, + B.APRVL_DOC_ID, + B.APRVL_STUS_CD, + B.APRVL_KIND_CD, + B.OFFER_DT, + B.CMPL_DT, + B.APRVL_CMPL_SNO, + B.FINAL_APRVL_SNO, + B.APLNT_ID, + U1.USR_NM AS APLNT_NM, + U2.LATE_SKIPOFF_ABTI_OT_DATE, + U2.NML_WORK_RCNTN_YN, + U2.LATE_SKIPOFF_OT_DT, + U3.UPD_DATE_YC, + U3.FREE_CNT, + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_DATE_CHANGE, + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + B.APLNT_CN , + B.BZ_CN, + B.OFFER_CN, + B.UPD_DT + + FROM SX_GW0090 B + LEFT OUTER JOIN + ( SELECT * FROM SX_GW0100 + WHERE 1=1 + AND CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + CONVERT(VARCHAR, APRVL_SNO) IN + (SELECT CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) + '|' + MAX(CONVERT(VARCHAR, APRVL_SNO)) + FROM SX_GW0100 + WHERE CORP_NO = @P0 + GROUP BY CORP_NO, APRVL_DOC_ID ) + + ) A + ON (1=1 + AND B.CORP_NO = A.CORP_NO + AND B.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = B.CORP_NO + AND U1.USR_ID = B.APLNT_ID + ) + + LEFT JOIN SX_GW0080 U2 --SX_지각조퇴기록 + ON(1=1 + AND U2.CORP_NO = B.CORP_NO + AND U2.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN (SELECT TA.CORP_NO + , APRVL_DOC_ID + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23) + '(' + UPD_WORK_CD + ')') AS UPD_DATE_YC --연차 + + , CONVERT (VARCHAR, SUM(TB.YYCT_DEDU_DAY)) + '일' AS FREE_CNT --연차 일수 + + , MIN(CONVERT(VARCHAR, CONVERT(DATETIME, UPD_DATE), 23)) AS UPD_DATE_CHANGE --근무변경 + , MIN(UPD_BF_WORK_CD) AS UPD_BF_WORK_CD --변경전 근무 코드 + , MIN(UPD_WORK_CD) AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 TA LEFT JOIN SX_CO0070 TB ON TB.CORP_NO = TA.CORP_NO AND TB.WORK_CD = TA.UPD_WORK_CD + GROUP BY TA.CORP_NO, TA.APRVL_DOC_ID + ) U3 + ON(1=1 + AND U3.CORP_NO = B.CORP_NO + AND U3.APRVL_DOC_ID = B.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = B.BZ_DEPUTY_ID ) + + + WHERE 1=1 + AND B.CORP_NO = @P0 + AND B.APLNT_ID = @P1 + AND (NULLIF(@P2, '') IS NULL OR B.APRVL_STUS_CD = @P3) + AND + ( + (NULLIF(@P4, '') IS NULL OR (LEN(@P11) = 4 AND B.APRVL_KIND_CD = @P5 ) ) + OR + (NULLIF(@P12, '') IS NULL OR (LEN(@P13) > 4 AND B.APRVL_KIND_CD IN('0002', '0003', '0008', '0004') ) ) + ) + AND CONVERT(VARCHAR, ISNULL(B.OFFER_DT, B.RGST_DT), 112) BETWEEN @P6 AND @P7 + + ORDER BY + ISNULL(B.OFFER_DT, A.RGST_DT) DESC + + + OFFSET ((@P8 - 1) * @P9) ROWS + FETCH NEXT @P10 ROWS ONLY + + + ) A + */ + + + + --기존에 사용한 문장 BACKUP + --declare @p1 int + --set @p1=1492 + --exec sp_prepexec @p1 output + + --@P0 nvarchar(4000) + --,@P1 nvarchar(4000) + --,@P2 nvarchar(4000) + --,@P3 nvarchar(4000) + --,@P4 nvarchar(4000) + --,@P5 nvarchar(4000) + --,@P6 nvarchar(4000) + --,@P7 nvarchar(4000) + --,@P8 int + --,@P9 int + --,@P10 int + + --SELECT + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- CONVERT(VARCHAR, A.OFFER_DT, 120) AS OFFER_DT, + -- CONVERT(VARCHAR, A.CMPL_DT, 120) AS CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + -- (SELECT TOP 1 USR_NM FROM SX_GW0010 WHERE USR_ID = A.APLNT_ID) AS APLNT_NM + -- FROM ( + + -- SELECT + -- A.APRVL_DOC_ID, + -- A.APRVL_STUS_CD, + -- A.APRVL_KIND_CD, + -- A.OFFER_DT, + -- A.CMPL_DT, + -- A.APRVL_CMPL_SNO, + -- A.FINAL_APRVL_SNO, + -- A.APLNT_ID + + + -- FROM SX_GW0090 A + -- WHERE 1=1 + -- AND A.CORP_NO = @P0 + -- AND A.APLNT_ID = @P1 + -- AND (NULLIF(@P2, '') IS NULL OR A.APRVL_STUS_CD = @P3) + -- AND (NULLIF(@P4, '') IS NULL OR A.APRVL_KIND_CD = @P5) + -- AND CONVERT(VARCHAR, ISNULL(A.OFFER_DT, A.RGST_DT), 112) BETWEEN @P6 AND @P7 + + + -- ORDER BY + -- ISNULL(A.OFFER_DT, A.RGST_DT) DESC + + + --OFFSET ((@P8 - 1) * @P9) ROWS + --FETCH NEXT @P10 ROWS ONLY + + -- ) A + + + --'1111','00001','','','','','20161215','20170215',1,20,20 + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_holiday_detail_list_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-08-11 +-- Create Description : 스카이브릿지 그룹웨어 연차등상세내역 리스트 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '', '', '' +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '강충우', '', '', '' +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '16', '', '' +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '', 'Y', '' + +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '', '', 'USR_NM' --정렬순서 : (이름), 직책, 업무별, 팀별, 업로드순 + +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '', '', 'DUTY_CD' --정렬순서 : 이름, (직책), 업무별, 팀별, 업로드순 + +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '', '', 'BZ_CD' --정렬순서 : 이름, 직책, (업무별), 팀별, 업로드순 + +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '', '', 'TEAM_CD' --정렬순서 : 이름, 직책, 업무별, (팀별), 업로드순 + +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2017', '', '', '', 'SORT_ODR' --정렬순서 : 이름, 직책, 업무별, 팀별, (업로드순) + +-- exec pr_skybridge_gw_holiday_detail_list_select '1111', '2018', '정진구', '', '', 'SORT_ODR' --정렬순서 : 이름, 직책, 업무별, 팀별, (업로드순) + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_holiday_detail_list_select] + + + @CORP_NO VARCHAR(15) --회사코드 + , @YYVCT_YY VARCHAR(20) --조회년도 + , @USR_NM VARCHAR(50) --이름 + , @TEAM_CD VARCHAR(50) --팀코드 + , @RETIRE_YN VARCHAR(10) --퇴사여부 + , @ORDER_TYPE VARCHAR(10) --정렬방법 + + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + SELECT A.USR_ID, + A.USR_ID AS ORDER_2, + A.USR_NM, + A.BZ_CD, + A.TEAM_CD, + A.DUTY_CD, + @YYVCT_YY AS YYVCT_YY, + + ISNULL(B.YYVCT_CNT,0) AS YYVCT_CNT, --(A)총연차 + ISNULL(COM_CNT,0) AS COM_CNT, --(B)승인연차 + ISNULL(NONCOM_CNT,0) AS NONCOM_CNT, --(C)비승인연차 + ISNULL(ABSENT_CNT,0) AS ABSENT_CNT, --(D)출퇴근미기록 + ISNULL(B.YYVCT_CNT,0) - ISNULL(COM_CNT,0) + - ISNULL(NONCOM_CNT,0) + - ISNULL(ABSENT_CNT,0) AS REMAIN_CNT, --(S)잔여연차 S=A-B-C-D + + B.SORT_ODR, + 0 AS ORI + + FROM SX_GW0010 A + LEFT JOIN SX_GW0060 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.USR_ID = A.USR_ID + AND B.YYVCT_YY = @YYVCT_YY ) + + --승인 연차 일수 + LEFT OUTER JOIN ( + SELECT A.CORP_NO, + A.USR_ID, + + SUM(B.YYCT_DEDU_DAY) AS COM_CNT --YYCT_DEDU_DCNT, --연차사용일수 + + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(A.WORK_CD, A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + --AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND A.WORK_PLAN_YYMMDD IN ( SELECT T2.UPD_DATE + FROM SX_GW0090 T1 + INNER JOIN SX_GW0110 T2 ON (T1.CORP_NO = T2.CORP_NO + AND T1.APRVL_DOC_ID = T2.APRVL_DOC_ID ) + WHERE T1.APRVL_STUS_CD = '0003' --결재완료 + AND T1.CORP_NO = A.CORP_NO + AND T1.APLNT_ID = A.USR_ID + ) + GROUP BY + A.CORP_NO, + A.USR_ID + ) T2 + + ON( 1=1 + AND T2.CORP_NO = A.CORP_NO + AND T2.USR_ID = A.USR_ID + ) + + --비승인 연차 일수 + LEFT OUTER JOIN ( + SELECT A.CORP_NO, + A.USR_ID, + + SUM(B.YYCT_DEDU_DAY) AS NONCOM_CNT --비승인연차일수 + + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + --AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND A.WORK_PLAN_YYMMDD NOT IN ( SELECT T2.UPD_DATE + FROM SX_GW0090 T1 + INNER JOIN SX_GW0110 T2 ON (T1.CORP_NO = T2.CORP_NO + AND T1.APRVL_DOC_ID = T2.APRVL_DOC_ID ) + WHERE T1.APRVL_STUS_CD = '0003' --결재완료 + AND T1.CORP_NO = A.CORP_NO + AND T1.APLNT_ID = A.USR_ID + ) + GROUP BY + A.CORP_NO, + A.USR_ID + ) T31 + + ON( 1=1 + AND T31.CORP_NO = A.CORP_NO + AND T31.USR_ID = A.USR_ID + ) + + --출퇴근 미기록 일수 + --출퇴근 미기록은 연차(V), '병', 특별휴무(X), 예비군훈련(Y)은 제외한 일자만 표시 한다. + LEFT OUTER JOIN ( + SELECT CORP_NO + , USR_ID + , COUNT('A') ABSENT_CNT + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + AND WORK_START_DT IS NULL + AND WORK_END_DT IS NULL + AND ISNULL(WORK_CD, PLAN_WORK_CD) NOT IN ('W', 'V', '병', 'X', 'Y', 'U') --정기휴일, 연차, 병가, 특별휴무, 예비군훈련은 제외한다.(20170425추가) + GROUP BY CORP_NO, USR_ID + ) T6 + ON (1=1 + AND T6.CORP_NO = A.CORP_NO + AND T6.USR_ID = A.USR_ID + ) + + + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND (NULLIF(@USR_NM, '') IS NULL OR A.USR_NM LIKE CONCAT('%', @USR_NM, '%')) + AND (NULLIF(@TEAM_CD, '') IS NULL OR A.TEAM_CD = @TEAM_CD) + AND (@RETIRE_YN = 'Y' OR NULLIF(A.RETIREMENT_DATE, '') IS NULL) + + + --ordering을 parameter를 넘겨서 처리 가능 + ORDER BY CASE WHEN @ORDER_TYPE = 'USR_NM' THEN ISNULL(USR_NM, '') + WHEN @ORDER_TYPE = 'DUTY_CD' THEN ISNULL(DUTY_CD, '') + WHEN @ORDER_TYPE = 'BZ_CD' THEN ISNULL(BZ_CD, '') + WHEN @ORDER_TYPE = 'TEAM_CD' THEN ISNULL(TEAM_CD, '') + --WHEN @ORDER_TYPE = 'SORT_ODR' THEN ISNULL(SORT_ODR, 0) -- + ELSE '' + END + , B.SORT_ODR, ORDER_2 + + + + + --ORDER BY + -- + -- B.${user_order_by}, + -- + -- + -- A.${user_order_by}, + -- + -- ORDER_2 + -- + + + + +END --PROCEDURE END +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_holiday_management_detail_DocID_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-20 +-- Create Description : 스카이브릿지 연차등관리 상세 화면 조회-문서ID 추가 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_holiday_management_detail_select '1111','00001' , '2017' +-- exec pr_skybridge_gw_holiday_management_detail_select '1111','10004' , '2017' +-- exec pr_skybridge_gw_holiday_management_detail_select '1111','10216' , '2017' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_holiday_management_detail_DocID_select] + + @CORP_NO VARCHAR(15) --회사번호 + , @USR_ID VARCHAR(10) --사용자ID + , @YYVCT_YY VARCHAR(4) --연차년도 +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + -- 신규 작성 + SELECT T1.CORP_NO --회사번호 + , T1.USR_ID --사용자ID + , T1.USR_NM -- 이름 + -- , ISNULL(T2.YYVCT_CNT, 0) AS YYVCT_CNT -- 총연차일수 + + --, ISNULL(COM_CNT,0) AS COM_CNT --승인연차일수 + , ISNULL(COM_UPD_DATE,0) AS COM_UPD_DATE --승인연차일 + + --, ISNULL(NONCOM_CNT,0) AS NONCOM_CNT --비승인연차일수 + , NONCOM_UPD_DATE AS NONCOM_UPD_DATE --비승인연차일자 + + --, ISNULL(ABSENT_CNT, 0) AS ABSENT_CNT --출퇴근미기록일수 + --, ISNULL(ABSENT_DATE,0) AS ABSENT_DATE --출퇴근미기록일자 + + --, ISNULL(TG_CNT,0) AS TG_CNT --퇴근 미기록 일수 + --, TG_DATE AS TG_DATE --퇴근미기록일자 + + --, ISNULL(SPECIAL_CNT,0) AS SPECIAL_CNT --병가특별휴가일수 + --, ISNULL(SPECIAL_DATE,0) AS SPECIAL_DATE --병가특별휴가일자 + + -- , CONVERT(VARCHAR,ISNULL(T2.YYVCT_CNT, 0) - (ISNULL(COM_CNT,0) + ISNULL(NONCOM_CNT,0) + ISNULL(ABSENT_CNT, 0))) + ' = ' + + -- CONVERT(VARCHAR,ISNULL(T2.YYVCT_CNT, 0)) + ' - ( ' + + --CONVERT(VARCHAR,ISNULL(COM_CNT,0)) + ' + ' + + --CONVERT(VARCHAR,ISNULL(NONCOM_CNT,0))+ ' + ' + + --CONVERT(VARCHAR,ISNULL(ABSENT_CNT,0)) + ')' AS REAMIN_HOLIDAY --잔여휴가 = 총연차일 - (승인연차 + 비승인연차 + 출퇴근미기록) + + + FROM (SELECT * + FROM SX_GW0010 --사용자 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND USR_ID = @USR_ID + ) T1 + LEFT OUTER JOIN SX_GW0060 T2 --연차관리 + ON ( 1=1 + AND T2.CORP_NO = T1.CORP_NO + AND T2.USR_ID = T1.USR_ID + AND T2.YYVCT_YY = @YYVCT_YY + ) + LEFT OUTER JOIN ( + SELECT A.CORP_NO + , A.APLNT_ID AS USR_ID + -- , SUM(CASE WHEN UPD_WORK_CD IN ('V') THEN 1 --연차면 1일, + -- WHEN UPD_WORK_CD IN ('H', 'I', 'J') THEN 0.5 --반차면 0.5일을 뺀다 + --ELSE 0 END) AS COM_CNT --승인연차일수 + + FROM SX_GW0090 A + LEFT OUTER JOIN SX_GW0110 C + ON( 1=1 + AND C.CORP_NO = A.CORP_NO + AND C.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND SUBSTRING(C.UPD_DATE,1,4) = @YYVCT_YY --해당년도의 연차만 조회 + AND A.APRVL_KIND_CD = '0005' --연차등만 조회 + AND A.CORP_NO + '!' + CONVERT(VARCHAR,A.APRVL_DOC_ID) IN (SELECT CORP_NO + '!' + CONVERT(VARCHAR,APRVL_DOC_ID) + FROM SX_GW0100 + WHERE APPR_STUS_CD = '0003' --승인 완료 + AND RGSTR_ID = @USR_ID ) + GROUP BY A.CORP_NO, A.APLNT_ID + + ) T3 + ON (1=1 + AND T3.CORP_NO = T1.CORP_NO + AND T3.USR_ID = T1.USR_ID + ) + + + LEFT OUTER JOIN ( + SELECT A.CORP_NO + , A.APLNT_ID AS USR_ID + + , SUM(CASE WHEN UPD_WORK_CD IN ('V') THEN 1 --연차면 1일, + WHEN UPD_WORK_CD IN ('H', 'I', 'J') THEN 0.5 --반차면 0.5일을 뺀다 + ELSE 0 END) AS NONCOM_CNT --비승인연차일수 + + + FROM SX_GW0090 A + LEFT OUTER JOIN SX_GW0110 C + ON( 1=1 + AND C.CORP_NO = A.CORP_NO + AND C.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND SUBSTRING(C.UPD_DATE,1,4) = @YYVCT_YY --해당년도의 연차만 조회 + AND A.APRVL_KIND_CD = '0005' --연차등만 조회 + AND A.CORP_NO + '!' + CONVERT(VARCHAR,A.APRVL_DOC_ID) IN (SELECT CORP_NO + '!' + CONVERT(VARCHAR,APRVL_DOC_ID) + FROM SX_GW0100 + WHERE APPR_STUS_CD = '0002' --결재중 + AND RGSTR_ID = @USR_ID ) + GROUP BY A.CORP_NO, A.APLNT_ID + + ) T31 + ON (1=1 + AND T31.CORP_NO = T1.CORP_NO + AND T31.USR_ID = T1.USR_ID + ) + + + --승인완료 연차 일자 : 일1, 월2, 화3, 수4, 목5, 금6, 토7 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,UPD_DATE), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 7 THEN '토요일' + END + FROM SX_GW0110 AS TA INNER JOIN SX_GW0090 AS TB + ON TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_WORK_CD IN ('V', 'H', 'I', 'J') + AND SUBSTRING(TA.UPD_DATE,1,4) = @YYVCT_YY --당해년도 + AND TB.APRVL_KIND_CD = '0005' --결재문서가 '연차등'인 경우 + AND TA.CORP_NO + '!' + CONVERT(VARCHAR,TA.APRVL_DOC_ID) IN (SELECT CORP_NO + '!' + CONVERT(VARCHAR,APRVL_DOC_ID) + FROM SX_GW0100 + WHERE APPR_STUS_CD = '0003' --승인 완료 + AND RGSTR_ID = @USR_ID ) + + ORDER BY UPD_DATE + FOR XML PATH('') + + ),1,10,'') AS COM_UPD_DATE + FROM SX_GW0110 AS P + + ) T4 + ON( 1=1 + AND T4.CORP_NO = T1.CORP_NO + ) + + --비승인 연차 일자 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + , STUFF(( + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,UPD_DATE), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 7 THEN '토요일' + END + + FROM SX_GW0110 AS TA INNER JOIN SX_GW0090 AS TB + ON TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_WORK_CD IN ('V', 'H', 'I', 'J') + AND SUBSTRING(TA.UPD_DATE,1,4) = @YYVCT_YY --당해년도 + AND TB.APRVL_KIND_CD = '0005' --결재문서가 '연차등'인 경우 + AND TA.CORP_NO + '!' + CONVERT(VARCHAR,TA.APRVL_DOC_ID) IN (SELECT CORP_NO + '!' + CONVERT(VARCHAR,APRVL_DOC_ID) + FROM SX_GW0100 + WHERE APPR_STUS_CD = '0002' --결재중 + AND RGSTR_ID = @USR_ID ) + + + + ORDER BY UPD_DATE + FOR XML PATH('') + + ),1,10,'') AS NONCOM_UPD_DATE + FROM SX_GW0110 + + ) T5 + ON( 1=1 + AND T5.CORP_NO = T1.CORP_NO + ) + + --출퇴근 미기록 일수 + LEFT OUTER JOIN ( + SELECT CORP_NO + , USR_ID + , COUNT('A') ABSENT_CNT + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + AND WORK_START_DT IS NULL + AND WORK_END_DT IS NULL + GROUP BY CORP_NO, USR_ID + ) T6 + ON (1=1 + AND T6.CORP_NO = T1.CORP_NO + AND T6.USR_ID = T1.USR_ID + ) + + --출퇴근 미기록 일자 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + PLAN_WORK_CD + ')' + + FROM SX_GW0050 + WHERE CORP_NO + '!' + USR_ID + '!' + WORK_PLAN_YYMMDD IN + ( SELECT CORP_NO + '!' + USR_ID + '!' + WORK_PLAN_YYMMDD + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + AND WORK_START_DT IS NULL + AND WORK_END_DT IS NULL + AND USR_ID = @USR_ID + ) + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + ),1,10,'') AS ABSENT_DATE + FROM SX_GW0050 AS P + + ) T61 + ON( 1=1 + AND T61.CORP_NO = T1.CORP_NO + ) + + --퇴근 미기록 일수 + LEFT OUTER JOIN ( + SELECT CORP_NO + , USR_ID + , COUNT('A') TG_CNT + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND WORK_START_DT IS NOT NULL + AND WORK_END_DT IS NULL + GROUP BY CORP_NO, USR_ID + ) T7 + ON (1=1 + AND T7.CORP_NO = T1.CORP_NO + AND T7.USR_ID = T1.USR_ID + ) + + + --퇴근 미기록 일자 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + PLAN_WORK_CD + ')' + FROM SX_GW0050 + WHERE CORP_NO + '!' + USR_ID+ '!' + WORK_PLAN_YYMMDD IN + ( SELECT CORP_NO + '!' + USR_ID+ '!' + WORK_PLAN_YYMMDD + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND WORK_START_DT IS NOT NULL + AND WORK_END_DT IS NULL + AND USR_ID = @USR_ID + ) + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + ),1,10,'') AS TG_DATE + FROM SX_GW0050 AS P + + ) T71 + ON (1=1 + AND T71.CORP_NO = T1.CORP_NO + ) + + --병가, 특별휴가 일수 + LEFT OUTER JOIN ( + SELECT CORP_NO + , RGSTR_ID AS USR_ID + , COUNT('A') SPECIAL_CNT + FROM SX_GW0110 + WHERE SUBSTRING(UPD_DATE,1,4) = @YYVCT_YY + AND UPD_WORK_CD IN ('병', 'X', 'Y', 'Z') + AND CORP_NO + '!' + CONVERT(VARCHAR, APRVL_DOC_ID) IN (SELECT CORP_NO + '!' + CONVERT(VARCHAR,APRVL_DOC_ID) + FROM SX_GW0100 + WHERE APPR_STUS_CD = '0003' --승인 완료 + AND RGSTR_ID = @USR_ID ) + + GROUP BY CORP_NO, RGSTR_ID + ) T8 + ON (1=1 + AND T8.CORP_NO = T1.CORP_NO + AND T8.USR_ID = T1.USR_ID + ) + + --병가, 특별휴가 일자 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,UPD_DATE), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 7 THEN '토요일' + END + ' (' + UPD_WORK_CD + ')' + FROM SX_GW0110 + WHERE CORP_NO + '!' + RGSTR_ID + '!' + UPD_DATE IN + ( SELECT CORP_NO + '!' + RGSTR_ID + '!' + UPD_DATE + FROM SX_GW0110 + WHERE SUBSTRING(UPD_DATE,1,4) = @YYVCT_YY + AND UPD_WORK_CD IN ('병', 'X', 'Y', 'Z') + AND RGSTR_ID = @USR_ID + ) + AND CORP_NO + '!' + CONVERT(VARCHAR, APRVL_DOC_ID) IN (SELECT CORP_NO + '!' + CONVERT(VARCHAR,APRVL_DOC_ID) + FROM SX_GW0100 + WHERE APPR_STUS_CD = '0003' --승인 완료 + AND RGSTR_ID = @USR_ID ) + + ORDER BY UPD_DATE + FOR XML PATH('') + ),1,10,'') AS SPECIAL_DATE + FROM SX_GW0110 AS P + + ) T81 + ON (1=1 + AND T81.CORP_NO = T1.CORP_NO + ) + + + + WHERE 1=1 + AND T1.CORP_NO = @CORP_NO + AND T1.USR_ID = @USR_ID + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_holiday_management_detail_select_id] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-20 +-- Create Description : 스카이브릿지 연차등관리에서 일자를 가지고 결재문서 ID 조회하기 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_holiday_management_detail_select_id '1111','00001' , '20170301', '승인연차' +-- exec pr_skybridge_gw_holiday_management_detail_select_id '1111','10004' , '20170301', '비승인연차' +-- exec pr_skybridge_gw_holiday_management_detail_select_id '1111','10216' , '20170301', +-- exec pr_skybridge_gw_holiday_management_detail_select_id '1111','10027' , '20170301', +-- exec pr_skybridge_gw_holiday_management_detail_select_id '1111','00025' , '20170301', + + + +-- select * from SX_GW0010 where USR_NM like '안' + '%' +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_holiday_management_detail_select_id] + + @CORP_NO VARCHAR(15) --회사번호 + , @USR_ID VARCHAR(10) --사용자ID + , @APRVL_DATE VARCHAR(8) --결재일자 + , @GUBUN VARCHAR(50) --구분 : 승인연차, 비승인연차, 지각, 조퇴, 외출 +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + + IF @GUBUN IN ('승인연차') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0110 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + AND TB.APRVL_STUS_CD = '0003' --결재완료 + AND TB.APRVL_KIND_CD = '0005' --결재종류 : 연차등 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_DATE = @APRVL_DATE + + + ELSE IF @GUBUN IN ('비승인연차') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0110 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + AND TB.APRVL_STUS_CD = '0002' --결재중 + AND TB.APRVL_KIND_CD = '0005' --결재종류 : 연차등 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_DATE = @APRVL_DATE + + + ELSE IF @GUBUN IN ('병가/특별휴가') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0110 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + AND TB.APRVL_STUS_CD = '0003' --결재완료 + AND TB.APRVL_KIND_CD = '0005' --결재종류 : 연차등 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_DATE = @APRVL_DATE + + ELSE IF @GUBUN IN ('지각A') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0110 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + AND TB.APRVL_STUS_CD = '0003' --결재완료 + AND TB.APRVL_KIND_CD = '0002' --결재종류 : 지각 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_DATE = @APRVL_DATE + + ELSE IF @GUBUN IN ('조퇴A') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0110 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + AND TB.APRVL_STUS_CD = '0003' --결재완료 + AND TB.APRVL_KIND_CD = '0003' --결재종류 : 조퇴 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_DATE = @APRVL_DATE + + ELSE IF @GUBUN IN ('외출A') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0110 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + AND TB.APRVL_STUS_CD = '0003' --결재완료 + AND TB.APRVL_KIND_CD = '0008' --결재종류 : 외출 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.UPD_DATE = @APRVL_DATE + + + ELSE IF @GUBUN IN ('지각') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0050 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APLNT_ID = TA.RGSTR_ID + AND TB.APRVL_STUS_CD IN ('0001', '0002', '0003') + AND TB.APRVL_KIND_CD = '0002' --결재종류 : 지각 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.WORK_PLAN_YYMMDD = @APRVL_DATE + + + ELSE IF @GUBUN IN ('조퇴') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0050 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APLNT_ID = TA.RGSTR_ID + AND TB.APRVL_STUS_CD IN ('0001', '0002', '0003') + AND TB.APRVL_KIND_CD = '0003' --결재종류 : 조퇴 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.WORK_PLAN_YYMMDD = @APRVL_DATE + + + ELSE IF @GUBUN IN ('외출') + SELECT MAX(TB.APRVL_DOC_ID) AS APRVL_DOC_ID + FROM SX_GW0050 TA + INNER JOIN SX_GW0090 TB + ON (1=1 + AND TB.CORP_NO = TA.CORP_NO + AND TB.APLNT_ID = TA.RGSTR_ID + AND TB.APRVL_STUS_CD IN ('0001', '0002', '0003') + AND TB.APRVL_KIND_CD = '0008' --결재종류 : 외출 + AND TB.APLNT_ID = @USR_ID + ) + WHERE 1=1 + AND TA.CORP_NO = @CORP_NO + AND TA.RGSTR_ID = @USR_ID + AND TA.WORK_PLAN_YYMMDD = @APRVL_DATE + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_holiday_management_detail_select_new] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-20 +-- Create Description : 스카이브릿지 연차등관리 상세 화면 조회- 수정 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_holiday_management_detail_select_new '1111','00001' , '2017' +-- exec pr_skybridge_gw_holiday_management_detail_select_new '1111','10004' , '2017' +-- exec pr_skybridge_gw_holiday_management_detail_select_new '1111','10216' , '2017' +-- exec pr_skybridge_gw_holiday_management_detail_select_new '1111','10027' , '2017' +-- exec pr_skybridge_gw_holiday_management_detail_select_new '1111','00025' , '2017' + +--exec pr_skybridge_gw_holiday_management_detail_select_new '1111','00038' , '2020' +--select * from SX_GW0010 where USR_NM = '조유진' + +-- select * from SX_GW0010 where USR_NM like '안' + '%' +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_holiday_management_detail_select_new] + + @CORP_NO VARCHAR(15) --회사번호 + , @USR_ID VARCHAR(10) --사용자ID + , @YYVCT_YY VARCHAR(4) --연차년도 +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + -- 신규 작성 + SELECT T1.CORP_NO --회사번호 + , T1.USR_ID --사용자ID + , T1.USR_NM -- 이름 + , ISNULL(T11.YYVCT_CNT, 0) AS YYVCT_CNT -- 총연차일수 + + , ISNULL(COM_CNT,0) AS COM_CNT --승인연차일수 + , ISNULL(COM_UPD_DATE,0) AS COM_UPD_DATE --승인연차일 + + , ISNULL(NONCOM_CNT,0) AS NONCOM_CNT --비승인연차일수 + , NONCOM_UPD_DATE AS NONCOM_UPD_DATE --비승인연차일자 + + , ISNULL(ABSENT_CNT, 0) AS ABSENT_CNT --출퇴근미기록일수 + , ISNULL(ABSENT_DATE,0) AS ABSENT_DATE --출퇴근미기록일자 + + , ISNULL(TG_CNT,0) AS TG_CNT --퇴근 미기록 일수 + , TG_DATE AS TG_DATE --퇴근미기록일자 + + , ISNULL(T2.SPECIAL_CNT,0) AS SPECIAL_CNT --병가특별휴가일수 + , ISNULL(T81.SPECIAL_DATE,0) AS SPECIAL_DATE --병가특별휴가일자 + + , T2.LATE_DCNT AS LATE_DCNT --지각횟수 + , LATE_DATE --지각일자 + + , T2.SKIPOFF_DCNT AS SKIPOFF_DCNT --조퇴횟수 + , SKIPOFF_DATE--조퇴일자 + + , T2.OUTING_CNT AS OUTING_CNT --외출횟수 + , OUTING_DATE--외출일자 + + + , CONVERT(VARCHAR,ISNULL(T11.YYVCT_CNT, 0) - (ISNULL(COM_CNT,0) + ISNULL(NONCOM_CNT,0) + ISNULL(ABSENT_CNT, 0))) + ' = ' + + CONVERT(VARCHAR,ISNULL(T11.YYVCT_CNT, 0)) + ' - ( ' + + CONVERT(VARCHAR,ISNULL(COM_CNT,0)) + ' + ' + + CONVERT(VARCHAR,ISNULL(NONCOM_CNT,0))+ ' + ' + + CONVERT(VARCHAR,ISNULL(ABSENT_CNT,0)) + ')' AS REAMIN_HOLIDAY --잔여휴가 = 총연차일 - (승인연차 + 비승인연차 + 출퇴근미기록) + + + FROM (SELECT * + FROM SX_GW0010 --사용자 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND USR_ID = @USR_ID + ) T1 + + LEFT OUTER JOIN + --INNER JOIN + ( SELECT * + FROM SX_GW0060 --총연차 + WHERE 1=1 + AND CORP_NO = @CORP_NO + AND USR_ID = @USR_ID + AND YYVCT_YY = @YYVCT_YY + ) T11 + ON (1=1 + AND T11.CORP_NO = T1.CORP_NO + AND T11.USR_ID = T1.USR_ID + ) + + --승인 연차 + LEFT OUTER JOIN ( + --INNER JOIN ( + SELECT A.CORP_NO, + A.USR_ID, + + --SUM(CASE WHEN ISNULL(NULLIF(A.ABTI_YN, ''), 'N') != 'Y' THEN B.APLY_WORK_DAY END) AS WORK_DCNT, --근무 + + SUM(B.YYCT_DEDU_DAY) AS COM_CNT, --YYCT_DEDU_DCNT, --연차사용일수 + + --COUNT(CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) = '결' OR A.ABTI_YN = 'Y' THEN 1 END) AS ABTI_DCNT, --결근 + + COUNT(CASE WHEN A.LATE_MIN > 0 THEN 1 END) AS LATE_DCNT, --지각 + + --조퇴는 오후반차(H), 야근반차(J) 인경우 제외한다(20170425) + COUNT(CASE WHEN A.SKIPOFF_REMN_BZ_MIN > 0 AND ISNULL(A.WORK_CD, A.PLAN_WORK_CD) NOT IN ('H', 'J') THEN 1 END) AS SKIPOFF_DCNT, --조퇴 + + + SUM(A.OUTING_CNT) AS OUTING_CNT, --외출횟수 + + SUM(A.OUTING_MIN) AS OUTING_MIN, --외출시간(분) + + COUNT(CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) IN ('병', 'X', 'Y') THEN 1 END) AS SPECIAL_CNT --SICKLEAVE_DCNT --병가, 특별휴가 일수 + + --SUM(A.TOT_WORK_MIN) AS TOT_WORK_MIN, + --SUM(A.LATE_MIN) AS LATE_MIN, + --SUM(A.SKIPOFF_REMN_BZ_MIN) AS SKIPOFF_REMN_BZ_MIN, + --SUM(A.INCLU_WORK_OT_MIN) AS INCLU_WORK_OT_MIN, + --SUM(A.OT_WORK_MIN) AS OT_WORK_MIN, + --SUM(A.NGT_OT_MIN) AS NGT_OT_MIN + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND A.WORK_PLAN_YYMMDD IN ( SELECT T2.UPD_DATE + FROM SX_GW0090 T1 + INNER JOIN SX_GW0110 T2 ON (T1.CORP_NO = T2.CORP_NO + AND T1.APRVL_DOC_ID = T2.APRVL_DOC_ID ) + WHERE T1.APRVL_STUS_CD = '0003' --결재완료 + AND T1.CORP_NO = @CORP_NO + AND T1.APLNT_ID = @USR_ID + ) + GROUP BY + A.CORP_NO, + A.USR_ID + ) T2 + + ON( 1=1 + AND T2.CORP_NO = T1.CORP_NO + AND T2.USR_ID = T1.USR_ID + ) + --승인완료 연차 일자 : 일1, 월2, 화3, 수4, 목5, 금6, 토7 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ')' + + + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + --AND B.WORK_CD IN ('V', 'H', 'I', 'J') --연차, 반차 + AND B.WORK_CD IN(SELECT WORK_CD FROM SX_CO0070 WHERE ISNULL(YYCT_DEDU_DAY, 0) > 0) + AND A.WORK_PLAN_YYMMDD IN ( SELECT T2.UPD_DATE + FROM SX_GW0090 T1 + INNER JOIN SX_GW0110 T2 ON (T1.CORP_NO = T2.CORP_NO + AND T1.APRVL_DOC_ID = T2.APRVL_DOC_ID ) + WHERE T1.APRVL_STUS_CD = '0003' --결재완료 + AND T1.CORP_NO = @CORP_NO + AND T1.APLNT_ID = @USR_ID + ) + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + + ),1,10,'') AS COM_UPD_DATE + FROM SX_CO0070 AS P + + ) T4 + ON( 1=1 + AND T4.CORP_NO = T1.CORP_NO + ) + + + + + --비승인 연차 + LEFT OUTER JOIN ( + --INNER JOIN ( + SELECT A.CORP_NO, + A.USR_ID, + + --SUM(CASE WHEN ISNULL(NULLIF(A.ABTI_YN, ''), 'N') != 'Y' THEN B.APLY_WORK_DAY END) AS WORK_DCNT, --근무 + + SUM(B.YYCT_DEDU_DAY) AS NONCOM_CNT , --비승인연차일수 + + --COUNT(CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) = '결' OR A.ABTI_YN = 'Y' THEN 1 END) AS ABTI_DCNT, --결근 + + COUNT(CASE WHEN A.LATE_MIN > 0 THEN 1 END) AS LATE_DCNT, --지각 + + --조퇴는 오후반차(H), 야근반차(J) 인경우 제외한다(20170425) + COUNT(CASE WHEN A.SKIPOFF_REMN_BZ_MIN > 0 AND ISNULL(A.WORK_CD, A.PLAN_WORK_CD) NOT IN ('H', 'J') THEN 1 END) AS SKIPOFF_DCNT, --조퇴 + + + SUM(A.OUTING_CNT) AS OUTING_CNT, --외출횟수 + + SUM(A.OUTING_MIN) AS OUTING_MIN, --외출시간(분) + + COUNT(CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) IN ('병', 'X', 'Y') THEN 1 END) AS SPECIAL_CNT --SICKLEAVE_DCNT --병가, 특별휴가 일수 + + --SUM(A.TOT_WORK_MIN) AS TOT_WORK_MIN, + --SUM(A.LATE_MIN) AS LATE_MIN, + --SUM(A.SKIPOFF_REMN_BZ_MIN) AS SKIPOFF_REMN_BZ_MIN, + --SUM(A.INCLU_WORK_OT_MIN) AS INCLU_WORK_OT_MIN, + --SUM(A.OT_WORK_MIN) AS OT_WORK_MIN, + --SUM(A.NGT_OT_MIN) AS NGT_OT_MIN + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND A.WORK_PLAN_YYMMDD NOT IN ( SELECT T2.UPD_DATE + FROM SX_GW0090 T1 + INNER JOIN SX_GW0110 T2 ON (T1.CORP_NO = T2.CORP_NO + AND T1.APRVL_DOC_ID = T2.APRVL_DOC_ID ) + WHERE T1.APRVL_STUS_CD = '0003' --결재완료 + AND T1.CORP_NO = @CORP_NO + AND T1.APLNT_ID = @USR_ID + ) + GROUP BY + A.CORP_NO, + A.USR_ID + ) T31 + + ON( 1=1 + AND T31.CORP_NO = T1.CORP_NO + AND T31.USR_ID = T1.USR_ID + ) + --비승인연차 일자 : 일1, 월2, 화3, 수4, 목5, 금6, 토7 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ')' + + + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + --AND B.WORK_CD IN ('V', 'H', 'I', 'J') --연차, 반차 + AND B.WORK_CD IN(SELECT WORK_CD FROM SX_CO0070 WHERE ISNULL(YYCT_DEDU_DAY, 0) > 0) + AND A.WORK_PLAN_YYMMDD NOT IN ( SELECT T2.UPD_DATE + FROM SX_GW0090 T1 + INNER JOIN SX_GW0110 T2 ON (T1.CORP_NO = T2.CORP_NO + AND T1.APRVL_DOC_ID = T2.APRVL_DOC_ID ) + WHERE T1.APRVL_STUS_CD = '0003' --결재완료 + AND T1.CORP_NO = @CORP_NO + AND T1.APLNT_ID = @USR_ID + ) + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + + ),1,10,'') AS NONCOM_UPD_DATE + FROM SX_CO0070 AS P + + ) T5 + ON( 1=1 + AND T4.CORP_NO = T1.CORP_NO + ) + + + + + -- --비승인 연차 일수 - OLD + -- LEFT OUTER JOIN ( + -- SELECT A.CORP_NO + -- , A.APLNT_ID AS USR_ID + + -- , SUM(CASE WHEN UPD_WORK_CD IN ('V') THEN 1 --연차면 1일, + -- WHEN UPD_WORK_CD IN ('H', 'I', 'J') THEN 0.5 --반차면 0.5일을 뺀다 + -- ELSE 0 END) AS NONCOM_CNT --비승인연차일수 + + + -- FROM SX_GW0090 A + -- LEFT OUTER JOIN SX_GW0110 C + -- ON( 1=1 + -- AND C.CORP_NO = A.CORP_NO + -- AND C.APRVL_DOC_ID = A.APRVL_DOC_ID + -- ) + + -- WHERE 1=1 + -- AND A.CORP_NO = @CORP_NO + -- AND A.APLNT_ID = @USR_ID + -- AND SUBSTRING(C.UPD_DATE,1,4) = @YYVCT_YY --해당년도의 연차만 조회 + -- AND A.APRVL_KIND_CD = '0005' --연차등만 조회 + -- AND A.APRVL_STUS_CD = '0002' --결재중 + -- GROUP BY A.CORP_NO, A.APLNT_ID + + -- ) T31 + --ON (1=1 + -- AND T31.CORP_NO = T1.CORP_NO + -- AND T31.USR_ID = T1.USR_ID + -- ) + + -- --비승인 연차 일자 + -- LEFT OUTER JOIN ( + -- SELECT DISTINCT CORP_NO + -- , STUFF(( + -- SELECT '
' + + -- CONVERT(VARCHAR, CONVERT(DATETIME,UPD_DATE), 23) + ' ' + + -- CASE WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 1 THEN '일요일' + -- WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 2 THEN '월요일' + -- WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 3 THEN '화요일' + -- WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 4 THEN '수요일' + -- WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 5 THEN '목요일' + -- WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 6 THEN '금요일' + -- WHEN DATEPART (DW, CONVERT(DATETIME,UPD_DATE)) = 7 THEN '토요일' + -- END + ' (' + ISNULL(NULLIF(UPD_WORK_CD, ''), UPD_BF_WORK_CD) + ')' + + -- FROM SX_GW0110 AS TA INNER JOIN SX_GW0090 AS TB + -- ON TB.CORP_NO = TA.CORP_NO + -- AND TB.APRVL_DOC_ID = TA.APRVL_DOC_ID + -- WHERE 1=1 + -- AND TA.CORP_NO = @CORP_NO + -- AND TB.APLNT_ID = @USR_ID + -- AND TA.UPD_WORK_CD IN ('V', 'H', 'I', 'J') + -- AND SUBSTRING(TA.UPD_DATE,1,4) = @YYVCT_YY --당해년도 + -- AND TB.APRVL_KIND_CD = '0005' --결재문서가 '연차등'인 경우 + -- AND TB.APRVL_STUS_CD = '0002' --결재중 + + -- ORDER BY UPD_DATE + -- FOR XML PATH('') + + -- ),1,10,'') AS NONCOM_UPD_DATE + -- FROM SX_GW0110 + + -- ) T5 + -- ON( 1=1 + -- AND T5.CORP_NO = T1.CORP_NO + -- ) + + --출퇴근 미기록 일수 + --출퇴근 미기록은 연차(V), '병', 특별휴무(X), 예비군훈련(Y)은 제외한 일자만 표시 한다. + LEFT OUTER JOIN ( + SELECT CORP_NO + , USR_ID + , COUNT('A') ABSENT_CNT + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + /* + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + */ + AND ISNULL(WORK_CD, PLAN_WORK_CD) >= 'A' + AND ISNULL(WORK_CD, PLAN_WORK_CD) <= 'T' + + AND WORK_START_DT IS NULL + AND WORK_END_DT IS NULL + AND ISNULL(WORK_CD, PLAN_WORK_CD) NOT IN ('V', '병', 'X', 'Y') --연차, 병가, 특별휴무, 예비군훈련은 제외한다.(20170425추가) + GROUP BY CORP_NO, USR_ID + ) T6 + ON (1=1 + AND T6.CORP_NO = T1.CORP_NO + AND T6.USR_ID = T1.USR_ID + ) + + --출퇴근 미기록 일자 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + SELECT '
' + + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + PLAN_WORK_CD + ')' + + FROM SX_GW0050 + WHERE CORP_NO + '!' + USR_ID + '!' + WORK_PLAN_YYMMDD IN + ( SELECT CORP_NO + '!' + USR_ID + '!' + WORK_PLAN_YYMMDD + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND ISNULL(WORK_CD, PLAN_WORK_CD) >= 'A' + AND ISNULL(WORK_CD, PLAN_WORK_CD) <= 'T' + AND WORK_START_DT IS NULL + AND WORK_END_DT IS NULL + AND USR_ID = @USR_ID + AND ISNULL(WORK_CD, PLAN_WORK_CD) NOT IN ('V', '병', 'X', 'Y') --연차, 병가, 특별휴무, 예비군훈련은 제외한다.(20170425추가) + + ) + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + ),1,10,'') AS ABSENT_DATE + FROM SX_GW0050 AS P + + ) T61 + ON( 1=1 + AND T61.CORP_NO = T1.CORP_NO + ) + + --퇴근 미기록 일수 + LEFT OUTER JOIN ( + SELECT CORP_NO + , USR_ID + , COUNT('A') TG_CNT + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND WORK_START_DT IS NOT NULL + AND WORK_END_DT IS NULL + GROUP BY CORP_NO, USR_ID + ) T7 + ON (1=1 + AND T7.CORP_NO = T1.CORP_NO + AND T7.USR_ID = T1.USR_ID + ) + + + --퇴근 미기록 일자 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + PLAN_WORK_CD + ')' + FROM SX_GW0050 + WHERE CORP_NO + '!' + USR_ID+ '!' + WORK_PLAN_YYMMDD IN + ( SELECT CORP_NO + '!' + USR_ID+ '!' + WORK_PLAN_YYMMDD + FROM SX_GW0050 + WHERE SUBSTRING(WORK_PLAN_YYMMDD,1,4) = @YYVCT_YY + AND PLAN_WORK_CD >= 'A' + AND PLAN_WORK_CD <= 'T' + AND WORK_PLAN_YYMMDD < CONVERT(VARCHAR, GETDATE(), 112)--오늘일자 보다 하루 적은 날자로 가져온다. + AND WORK_START_DT IS NOT NULL + AND WORK_END_DT IS NULL + AND USR_ID = @USR_ID + ) + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + ),1,10,'') AS TG_DATE + FROM SX_GW0050 AS P + + ) T71 + ON (1=1 + AND T71.CORP_NO = T1.CORP_NO + ) + + --병가, 특별휴가 일자 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ')' + + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND B.WORK_CD IN ('병', 'X', 'Y', 'Z') + + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + + ),1,10,'') AS SPECIAL_DATE + FROM SX_CO0070 AS P + + ) T81 + ON (1=1 + AND T81.CORP_NO = T1.CORP_NO + ) + + + --지각 일자 : 일1, 월2, 화3, 수4, 목5, 금6, 토7 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ')' + + + FROM SX_GW0050 A + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND A.LATE_MIN > 0 + + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + + ),1,10,'') AS LATE_DATE + FROM SX_GW0050 AS P + + ) T91 + ON( 1=1 + AND T91.CORP_NO = T1.CORP_NO + ) + + + --조퇴 일자 : 일1, 월2, 화3, 수4, 목5, 금6, 토7 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ')' + + + FROM SX_GW0050 A + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND A.SKIPOFF_REMN_BZ_MIN > 0 + AND ISNULL(A.WORK_CD, A.PLAN_WORK_CD) NOT IN ('H', 'J') --조퇴는 오후반차(H), 야근반차(J) 인경우 제외한다(20170425) + + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + + ),1,10,'') AS SKIPOFF_DATE + FROM SX_GW0050 AS P + + ) T92 + ON( 1=1 + AND T92.CORP_NO = T1.CORP_NO + ) + + + --외출 일자 : 일1, 월2, 화3, 수4, 목5, 금6, 토7 + LEFT OUTER JOIN ( + SELECT DISTINCT CORP_NO + ,STUFF(( + + SELECT '
' + CONVERT(VARCHAR, CONVERT(DATETIME,WORK_PLAN_YYMMDD), 23) + ' ' + + CASE WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 1 THEN '일요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 2 THEN '월요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 3 THEN '화요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 4 THEN '수요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 5 THEN '목요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 6 THEN '금요일' + WHEN DATEPART (DW, CONVERT(DATETIME,WORK_PLAN_YYMMDD)) = 7 THEN '토요일' + END + ' (' + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ')' + + + FROM SX_GW0050 A + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND A.OUTING_CNT >= 1 + + ORDER BY WORK_PLAN_YYMMDD + FOR XML PATH('') + + ),1,10,'') AS OUTING_DATE + FROM SX_GW0050 AS P + + ) T93 + ON( 1=1 + AND T93.CORP_NO = T1.CORP_NO + ) + + WHERE 1=1 + AND T1.CORP_NO = @CORP_NO + AND T1.USR_ID = @USR_ID + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_holiday_request_detail_select_new] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 연차등신청 상세 화면 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_holiday_request_detail_select_new '1111','10027' , '2017' --안순회씨 + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_holiday_request_detail_select_new] + + @CORP_NO VARCHAR(15) --회사번호 + , @USR_ID VARCHAR(10) --사용자ID + , @YYVCT_YY VARCHAR(4) --연차년도 + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + + + SELECT + --T1.* , + --T2.* , + + T1.CORP_NO + , T1.USR_ID + , T1.WORK_CD + , T1.FREE_CNT + , T1.WORK_PLAN_YYMMDD + , T2.APRVL_STUS_DS + , T2.APRVL_DOC_ID + , T2.APRVL_STUS_CD + , T2.APRVL_KIND_CD + , T2.OFFER_DT + , T2.CMPL_DT + , T2.APRVL_CMPL_SNO + , T2.FINAL_APRVL_SNO + --, T2.USER_ID + , T2.APLNT_NM + , T2.BZ_DEPUTY_NM + , T2.UPD_BF_WORK_CD + , ISNULL(T2.UPD_WORK_CD, WORK_CD) AS UPD_WORK_CD + , ISNULL(T2.APLNT_CN, '<<< 결재 필요 >>>') AS APLNT_CN + , T2.BZ_CN + , T2.OFFER_CN + , T2.UPD_DT + , T2.COMM_CD_NM + , ISNULL (T2.UPD_DATE,WORK_PLAN_YYMMDD) AS UPD_DATE + + FROM ( + SELECT A.CORP_NO, + A.USR_ID, + + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) AS WORK_CD, + ISNULL(B.YYCT_DEDU_DAY,0) AS FREE_CNT, --연차사용일수 + + WORK_PLAN_YYMMDD --연차일자 + + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) IN ('H', 'I', 'J', 'V', '병','U', 'X', 'Y') + + ) T1 + + left outer join + --inner JOIN + + ( + SELECT A.COMM_CD_NM AS APRVL_STUS_DS --결재 + , A.* + + FROM ( + SELECT A.CORP_NO, + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + A.OFFER_DT, + A.CMPL_DT, + A.APRVL_CMPL_SNO, + A.FINAL_APRVL_SNO, + A.APLNT_ID AS USR_ID, + U1.USR_NM AS APLNT_NM, + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + A.APLNT_CN , + A.BZ_CN, + A.OFFER_CN, + A.UPD_DT, + COMM_CD_NM, + UPD_DATE + + FROM (SELECT * + FROM SX_GW0090 + WHERE CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) IN ( SELECT CORP_NO + '|' + CONVERT(VARCHAR, MAX(APRVL_DOC_ID)) + FROM SX_GW0110 + WHERE CORP_NO = @CORP_NO + AND RGSTR_ID = @USR_ID + GROUP BY CORP_NO, UPD_DATE + ) + ) A --같은날 중복으로 결재신청이 될수 있으므로 마지막으로 신청한 것만 사용 + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = A.CORP_NO + AND U1.USR_ID = A.APLNT_ID + ) + + LEFT JOIN (SELECT CORP_NO + , APRVL_DOC_ID + , UPD_DATE AS UPD_DATE + + , UPD_BF_WORK_CD AS UPD_BF_WORK_CD --변경전 근무 코드 + , UPD_WORK_CD AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 + ) U3 + + + ON(1=1 + AND U3.CORP_NO = A.CORP_NO + AND U3.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = A.BZ_DEPUTY_ID ) + + LEFT JOIN SX_CO0040 COMMON + ON (1=1 + AND COMMON.CORP_NO = A.CORP_NO + AND COMMON.COMM_CL_CD = 'SX011' --결재상태코드 + AND COMMON.COMM_CD = APRVL_STUS_CD ) + + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.APLNT_ID = @USR_ID + AND SUBSTRING(UPD_DATE,1,4) = @YYVCT_YY + --AND APRVL_STUS_CD IN ('0003') --결재완료 + AND UPD_WORK_CD IN ('H', 'I', 'J', 'V', '병','U', 'X', 'Y') + + ) A + + ) T2 ON (1=1 + AND T1.CORP_NO = T2.CORP_NO + AND T1.USR_ID = T2.USR_ID + AND T1.WORK_PLAN_YYMMDD = T2.UPD_DATE + ) + + ORDER BY T1.WORK_PLAN_YYMMDD + + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_holiday_request_detail_select_total_new] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 연차등신청 상세 화면 합계 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_holiday_request_detail_select_total_new '1111','00001' , '2017' +-- exec pr_skybridge_gw_holiday_request_detail_select_total_new '1111','10004' , '2017' +-- exec pr_skybridge_gw_holiday_request_detail_select_total_new '1111','10027' , '2017' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_holiday_request_detail_select_total_new] + + @CORP_NO VARCHAR(15) --회사번호 + , @USR_ID VARCHAR(10) --사용자ID + , @YYVCT_YY VARCHAR(4) --연차년도 + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + -- 20170601 신규 작성 (pr_skybridge_gw_holiday_request_detail_select_new 과 같은 쿼리 사용 + SELECT '총연차일 : ' + CONVERT(VARCHAR, ISNULL(YYVCT_CNT * 1.00,0)) + '일' AS YYVCT_CNT --총연차일 + , '신청일수 : ' + CONVERT(VARCHAR, ISNULL(FREE_CNT,0)) + '일' AS SINCHUNG_CNT --신청일수 + , '사용일수 : ' + CONVERT(VARCHAR, ISNULL(FREE_CNT,0)) + '일' AS USD_CNT --사용일수 + , '특휴·병가등일수 : ' + CONVERT(VARCHAR, ISNULL(SPECIAL_CNT*1.00,0)) + '일' AS SPECIAL_CNT --특휴병가등일수 + , '잔여연차 : ' + CONVERT(VARCHAR, ISNULL(YYVCT_CNT,0) - ISNULL(FREE_CNT,0)) + '일' AS REMAIN_CNT --잔여연차 + + FROM ( + --pr_skybridge_gw_holiday_request_detail_select_new 과 같은 쿼리 + SELECT + MAX(T1.CORP_NO) AS CORP_NO + , SUM(T1.FREE_CNT) AS FREE_CNT + , SUM(T1.SPECIAL_CNT) AS SPECIAL_CNT + + + FROM ( + SELECT A.CORP_NO, + A.USR_ID, + + ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) AS WORK_CD, + ISNULL(B.YYCT_DEDU_DAY,0) AS FREE_CNT, --연차사용일수 + + CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) IN ('병', 'X', 'Y') THEN 1 ELSE 0 END AS SPECIAL_CNT, + + WORK_PLAN_YYMMDD --연차일자 + + FROM SX_GW0050 A + LEFT JOIN SX_CO0070 B + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.USR_ID = @USR_ID + AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + AND ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) IN ('H', 'I', 'J', 'V', '병','U', 'X', 'Y') + + ) T1 + + left outer JOIN + + ( + SELECT A.COMM_CD_NM AS APRVL_STUS_DS --결재 + , A.* + + FROM ( + SELECT A.CORP_NO, + A.APRVL_DOC_ID, + A.APRVL_STUS_CD, + A.APRVL_KIND_CD, + A.OFFER_DT, + A.CMPL_DT, + A.APRVL_CMPL_SNO, + A.FINAL_APRVL_SNO, + A.APLNT_ID AS USR_ID, + U1.USR_NM AS APLNT_NM, + U4.USR_NM AS BZ_DEPUTY_NM, + + U3.UPD_BF_WORK_CD, + U3.UPD_WORK_CD, + + A.APLNT_CN , + A.BZ_CN, + A.OFFER_CN, + A.UPD_DT, + COMM_CD_NM, + UPD_DATE + + FROM (SELECT * + FROM SX_GW0090 + WHERE CORP_NO + '|' + CONVERT(VARCHAR, APRVL_DOC_ID) IN ( SELECT CORP_NO + '|' + CONVERT(VARCHAR, MAX(APRVL_DOC_ID)) + FROM SX_GW0110 + WHERE CORP_NO = @CORP_NO + AND RGSTR_ID = @USR_ID + GROUP BY CORP_NO, UPD_DATE + ) + ) A --같은날 중복으로 결재신청이 될수 있으므로 마지막으로 신청한 것만 사용 + + LEFT JOIN SX_GW0010 U1 + ON(1=1 + AND U1.CORP_NO = A.CORP_NO + AND U1.USR_ID = A.APLNT_ID + ) + + LEFT JOIN (SELECT CORP_NO + , APRVL_DOC_ID + , UPD_DATE AS UPD_DATE + + , UPD_BF_WORK_CD AS UPD_BF_WORK_CD --변경전 근무 코드 + , UPD_WORK_CD AS UPD_WORK_CD --변경후 근무코드 + FROM SX_GW0110 + ) U3 + + + ON(1=1 + AND U3.CORP_NO = A.CORP_NO + AND U3.APRVL_DOC_ID = A.APRVL_DOC_ID + ) + + LEFT JOIN SX_GW0010 U4 + ON(1=1 + AND U4.USR_ID = A.BZ_DEPUTY_ID ) + + LEFT JOIN SX_CO0040 COMMON + ON (1=1 + AND COMMON.CORP_NO = A.CORP_NO + AND COMMON.COMM_CL_CD = 'SX011' --결재상태코드 + AND COMMON.COMM_CD = APRVL_STUS_CD ) + + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.APLNT_ID = @USR_ID + AND SUBSTRING(UPD_DATE,1,4) = @YYVCT_YY + --AND APRVL_STUS_CD IN ('0003') --결재완료 + AND UPD_WORK_CD IN ('H', 'I', 'J', 'V', '병','U', 'X', 'Y') + + ) A + + ) T2 ON (1=1 + AND T1.CORP_NO = T2.CORP_NO + AND T1.USR_ID = T2.USR_ID + AND T1.WORK_PLAN_YYMMDD = T2.UPD_DATE + ) + ) TX LEFT OUTER JOIN + ( SELECT * + FROM SX_GW0060 + WHERE CORP_NO = @CORP_NO + AND USR_ID = @USR_ID + AND YYVCT_YY = @YYVCT_YY ) TY ON (1=1) + + + + + --************************************************************************************************************************************** + -- 2017601 이전 버젼 + --SELECT '총연차일 : ' + CONVERT(VARCHAR, ISNULL(YYVCT_CNT * 1.00,0)) + '일' AS YYVCT_CNT --총연차일 + -- , '신청일수 : ' + CONVERT(VARCHAR, ISNULL(COM_CNT,0)) + '일' AS SINCHUNG_CNT --신청일수 + -- , '사용일수 : ' + CONVERT(VARCHAR, ISNULL(COM_CNT,0)) + '일' AS USD_CNT --사용일수 + -- , '특휴·병가등일수 : ' + CONVERT(VARCHAR, ISNULL(SPECIAL_CNT*1.00,0)) + '일' AS SPECIAL_CNT --특휴병가등일수 + -- , '잔여연차 : ' + CONVERT(VARCHAR, ISNULL(YYVCT_CNT,0) - ISNULL(COM_CNT,0)) + '일' AS REMAIN_CNT --잔여연차 + + -- FROM + -- ( SELECT * + -- FROM SX_GW0060 + -- WHERE CORP_NO = @CORP_NO + -- AND USR_ID = @USR_ID + -- AND YYVCT_YY = @YYVCT_YY ) A + + -- inner JOIN + + -- ( SELECT + -- SUM(B.YYCT_DEDU_DAY) AS COM_CNT, --YYCT_DEDU_DCNT, --연차사용일수 + + -- COUNT(CASE WHEN A.LATE_MIN > 0 THEN 1 END) AS LATE_DCNT, --지각 + -- COUNT(CASE WHEN A.SKIPOFF_REMN_BZ_MIN > 0 THEN 1 END) AS SKIPOFF_DCNT, --조퇴 + -- SUM(A.OUTING_CNT) AS OUTING_CNT, --외출횟수 + + -- SUM(A.OUTING_MIN) AS OUTING_MIN, --외출시간(분) + + -- COUNT(CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) IN ('병', 'X', 'Y') THEN 1 END) AS SPECIAL_CNT --SICKLEAVE_DCNT --병가, 특별휴가 일수 + + -- FROM SX_GW0050 A + -- LEFT JOIN SX_CO0070 B + -- ON(1=1 + -- AND B.CORP_NO = A.CORP_NO + -- AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + -- ) + -- WHERE 1=1 + -- AND A.CORP_NO = @CORP_NO + -- AND A.USR_ID = @USR_ID + -- AND A.WORK_PLAN_YYMMDD LIKE @YYVCT_YY + '%' + + -- ) B ON (1=1) + + -- WHERE 1=1 + + + + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_monthly_working_time_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-02-02 +-- Create Description : 스카이브릿지 그룹웨어 월별근무시간 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_monthly_working_time_select '1111',NULL,NULL,'','','','','','',NULL,'20170201','20170228','20170201' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_monthly_working_time_select] + + @P0 nvarchar(4000) + ,@P1 varchar(8000) + ,@P2 varchar(8000) + ,@P3 nvarchar(4000) + ,@P4 nvarchar(4000) + ,@P5 nvarchar(4000) + ,@P6 nvarchar(4000) + ,@P7 nvarchar(4000) + ,@P8 nvarchar(4000) + ,@P9 varchar(8000) + ,@P10 nvarchar(4000) + ,@P11 nvarchar(4000) + ,@P12 nvarchar(4000) + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + ; + + + WITH USER1 AS + ( + SELECT U.CORP_NO, + U.USR_ID, + U.USR_NM, + U.TEAM_CD, + U.DUTY_CD + FROM SX_GW0010 U + WHERE 1=1 + AND U.CORP_NO = @P0 + AND (NULLIF(@P1, '') IS NULL OR U.USR_ID = @P2) + AND (NULLIF(@P3, '') IS NULL OR U.USR_NM LIKE CONCAT('%', @P4, '%')) + AND (NULLIF(@P5, '') IS NULL OR U.TEAM_CD = @P6) + AND (NULLIF(@P7, '') IS NULL OR U.DUTY_CD = @P8) + AND (@P9 = 'Y' OR NULLIF(U.RETIREMENT_DATE, '') IS NULL) + ) + + SELECT U.USR_ID, + U.USR_NM, + U.TEAM_CD, + U.DUTY_CD, + + A.WORK_DCNT, --근무 + B.YYVCT_CNT, --연차(조회가 없음) + + + A.YYCT_DEDU_DCNT, --연차사용 + + A.ABTI_DCNT, --결근 + + A.LATE_DCNT, --지각 + A.SKIPOFF_DCNT, --조퇴 + A.SICKLEAVE_DCNT, --병가 + + A.TOT_WORK_MIN, + A.LATE_MIN, + A.SKIPOFF_REMN_BZ_MIN, + A.INCLU_WORK_OT_MIN, + A.OT_WORK_MIN, + A.NGT_OT_MIN + FROM USER1 U + + LEFT JOIN ( + SELECT A.CORP_NO, + A.USR_ID, + --일수 + SUM(CASE WHEN ISNULL(NULLIF(A.ABTI_YN, ''), 'N') != 'Y' THEN B.APLY_WORK_DAY END) AS WORK_DCNT, --근무 + --연차(조회가 없음) + + SUM(B.YYCT_DEDU_DAY) AS YYCT_DEDU_DCNT, --연차사용 + + COUNT(CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) = '결' OR A.ABTI_YN = 'Y' THEN 1 END) AS ABTI_DCNT, --결근 + + COUNT(CASE WHEN A.LATE_MIN > 0 THEN 1 END) AS LATE_DCNT, --지각 + COUNT(CASE WHEN A.SKIPOFF_REMN_BZ_MIN > 0 THEN 1 END) AS SKIPOFF_DCNT, --조퇴 + COUNT(CASE WHEN ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) = '병' THEN 1 END) AS SICKLEAVE_DCNT, --병가 + + --(분) : 시간은 화면에서 계속하는것 같음 + SUM(A.TOT_WORK_MIN) AS TOT_WORK_MIN, --총근무 + SUM(A.LATE_MIN) AS LATE_MIN, --지각 + SUM(A.SKIPOFF_REMN_BZ_MIN) AS SKIPOFF_REMN_BZ_MIN, --조퇴 + SUM(A.INCLU_WORK_OT_MIN) AS INCLU_WORK_OT_MIN, --포괄OT + SUM(A.OT_WORK_MIN) AS OT_WORK_MIN, --OT + SUM(A.NGT_OT_MIN) AS NGT_OT_MIN --야간 + FROM USER1 U + + JOIN SX_GW0050 A + ON(1=1 + AND A.CORP_NO = U.CORP_NO + AND A.USR_ID = U.USR_ID + AND A.WORK_PLAN_YYMMDD BETWEEN @P10 AND @P11 + ) + + LEFT JOIN SX_CO0070 B --공통코드 + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + ) + WHERE 1=1 + GROUP BY + A.CORP_NO, + A.USR_ID + ) A + ON(1=1 + AND A.CORP_NO = U.CORP_NO + AND A.USR_ID = U.USR_ID + ) + + LEFT JOIN SX_GW0060 B + ON(1=1 + AND B.CORP_NO = U.CORP_NO + AND B.USR_ID = U.USR_ID + AND B.YYVCT_YY = SUBSTRING(@P12, 1, 4) + ) + WHERE 1=1 + ORDER BY + USR_NM, + USR_ID + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_today_early_leave_YN_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 퇴근 클릭시 조퇴인지를 체크 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_today_early_leave_YN_select '1111', '20170215' , '10454' +-- exec pr_skybridge_gw_today_early_leave_YN_select '1111', '20170216' , '10454' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_today_early_leave_YN_select] + + @CORP_NO VARCHAR(15) --회사코드 + , @WORK_PLAN_YYMMDD VARCHAR(8) --근무일자(계획) + , @USR_ID VARCHAR(20) --사용자 + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + + SELECT COUNT('A') AS EARLY_LEAVE_YN --1이면 조퇴 + FROM ( + SELECT EARLY_LEAVE_MIN + , USR_NM + , TEAM_CD + , DUTY_CD + , WORK_END_DT + , USR_ID + + FROM ( + SELECT DATEDIFF( + MINUTE, + CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GETOFFWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')), + ISNULL(A.WORK_END_DT, GETDATE()) + ) AS EARLY_LEAVE_MIN, + U.USR_NM, + U.TEAM_CD, + U.DUTY_CD, + A.WORK_END_DT, + U.USR_ID + FROM SX_GW0050 A --일별근무계획 + JOIN SX_GW0010 U --사용자 + ON(1=1 + AND U.CORP_NO = A.CORP_NO + AND U.USR_ID = A.USR_ID + ) + JOIN SX_CO0070 B --공통근무코드 + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + AND NULLIF(B.GETOFFWORK_TM_NM, '') IS NOT NULL + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.WORK_PLAN_YYMMDD = @WORK_PLAN_YYMMDD + AND ISNULL(A.WORK_CD, A.PLAN_WORK_CD) NOT IN('N', 'Z') + AND ( + ( + 1=1 + AND SUBSTRING(B.GETOFFWORK_TM_NM, 1, 1) != 'T' + AND CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GETOFFWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) + > ISNULL(A.WORK_END_DT, GETDATE()) + ) + ) + ) AS TA + WHERE 1=1 + + ) TA + WHERE USR_ID = @USR_ID + AND EARLY_LEAVE_MIN < 0 + + + + + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_today_late_person_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 메인화면의 오늘 지각한사람(분) 조회 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_today_late_person_select '20160113', '20160113', '1111', '20170117' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_today_late_person_select] + + @DISML_DATE VARCHAR(8) --해고일자 + , @RETIREMENT_DATE VARCHAR(8) --퇴직일자 일자 + , @CORP_NO VARCHAR(15) --회사코드 + , @WORK_PLAN_YYMMDD VARCHAR(8) --근무일자(계획) + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + -- 신규 작성 : 출근시각이 있는 사람에 한해서만 지각자를 DISPLAY 한다. + SELECT LATE_MIN + , USR_NM + , TEAM_CD + , DUTY_CD + , WORK_START_DT + FROM ( + SELECT DATEDIFF( + MINUTE, + CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')), + ISNULL(A.WORK_START_DT, GETDATE()) + ) AS LATE_MIN, + U.USR_NM, + U.TEAM_CD, + U.DUTY_CD, + A.WORK_START_DT + FROM SX_GW0050 A --일별근무계획 + JOIN SX_GW0010 U --사용자 + ON(1=1 + AND U.CORP_NO = A.CORP_NO + AND U.USR_ID = A.USR_ID + AND (NULLIF(U.DISML_DATE, '') IS NULL OR U.DISML_DATE >= @DISML_DATE) --해고일자 + AND (NULLIF(U.RETIREMENT_DATE, '') IS NULL OR U.RETIREMENT_DATE >= @RETIREMENT_DATE) --퇴직일자 + ) + JOIN SX_CO0070 B --공통근무코드 + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + AND NULLIF(B.GOTOWORK_TM_NM, '') IS NOT NULL + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.WORK_PLAN_YYMMDD = @WORK_PLAN_YYMMDD + AND ( + ( + 1=1 + AND SUBSTRING(B.GOTOWORK_TM_NM, 1, 1) != 'T' + AND CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) + < ISNULL(A.WORK_START_DT, GETDATE()) + ) + ) + ) AS TA + WHERE 1=1 + AND TA.LATE_MIN > 0 + --AND TA.WORK_START_DT IS NOT NULL + ORDER BY + LATE_MIN DESC, + USR_NM + + + + + -- --기존에 사용한 문장 BACKUP + --SELECT DATEDIFF( + -- MINUTE, + -- CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')), + -- ISNULL(A.WORK_START_DT, GETDATE()) + -- ) AS LATE_MIN, + -- U.USR_NM, + -- U.TEAM_CD, + -- U.DUTY_CD, + -- A.WORK_START_DT + -- FROM SX_GW0050 A --일별근무계획 + -- JOIN SX_GW0010 U --사용자 + -- ON(1=1 + -- AND U.CORP_NO = A.CORP_NO + -- AND U.USR_ID = A.USR_ID + -- AND (NULLIF(U.DISML_DATE, '') IS NULL OR U.DISML_DATE >= @DISML_DATE) --해고일자 + -- AND (NULLIF(U.RETIREMENT_DATE, '') IS NULL OR U.RETIREMENT_DATE >= @RETIREMENT_DATE) --퇴직일자 + -- ) + -- JOIN SX_CO0070 B --공통근무코드 + -- ON(1=1 + -- AND B.CORP_NO = A.CORP_NO + -- AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + -- AND NULLIF(B.GOTOWORK_TM_NM, '') IS NOT NULL + -- ) + -- WHERE 1=1 + -- AND A.CORP_NO = @CORP_NO + -- AND A.WORK_PLAN_YYMMDD = @WORK_PLAN_YYMMDD + -- AND ( + -- ( + -- 1=1 + -- AND SUBSTRING(B.GOTOWORK_TM_NM, 1, 1) != 'T' + -- AND CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) + -- < ISNULL(A.WORK_START_DT, GETDATE()) + -- ) + -- ) + -- ORDER BY + -- LATE_MIN DESC, + -- USR_NM + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_today_late_YN_select] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 출근체크 클릭시 지작인지를 체크 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_today_late_YN_select '1111', '20170307' , '10352' +-- exec pr_skybridge_gw_today_late_YN_select '1111', '20170307' , '00026' +-- exec pr_skybridge_gw_today_late_YN_select '1111', '20170307' , '00001' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_today_late_YN_select] + + @CORP_NO VARCHAR(15) --회사코드 + , @WORK_PLAN_YYMMDD VARCHAR(8) --근무일자(계획) + , @USR_ID VARCHAR(20) --사용자 + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + + SELECT COUNT('A') AS LATE_YN --1이면 지각 + FROM ( + SELECT LATE_MIN + , USR_NM + , TEAM_CD + , DUTY_CD + , WORK_START_DT + , USR_ID + + FROM ( + SELECT DATEDIFF( + MINUTE, + CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, (CASE WHEN ISNULL(A.WORK_CD, A.PLAN_WORK_CD) IN('N', 'Z') THEN B.GETOFFWORK_TM_NM ELSE B.GOTOWORK_TM_NM END), '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')), + ISNULL(A.WORK_START_DT, GETDATE()) + ) AS LATE_MIN, + U.USR_NM, + U.TEAM_CD, + U.DUTY_CD, + A.WORK_START_DT, + U.USR_ID + FROM SX_GW0050 A --일별근무계획 + JOIN SX_GW0010 U --사용자 + ON(1=1 + AND U.CORP_NO = A.CORP_NO + AND U.USR_ID = A.USR_ID + ) + JOIN SX_CO0070 B --공통근무코드 + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + AND NULLIF(B.GOTOWORK_TM_NM, '') IS NOT NULL + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.WORK_PLAN_YYMMDD = @WORK_PLAN_YYMMDD + --AND ISNULL(A.WORK_CD, A.PLAN_WORK_CD) NOT IN('N', 'Z') --2017.06.15 지각계 작성 관련 작업 + AND ( + ( + 1=1 + --AND DATEPART(DW, @WORK_PLAN_YYMMDD) NOT IN('1', '7') --2017.06.15 지각계 작성 관련 작업 + AND SUBSTRING(B.GOTOWORK_TM_NM, 1, 1) != 'T' + AND CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) + < ISNULL(A.WORK_START_DT, GETDATE()) + ) + ) + ) AS TA + WHERE 1=1 + --AND TA.WORK_START_DT IS NOT NULL + + ) TA + WHERE USR_ID = @USR_ID + AND LATE_MIN >= 1 + + + + + -- --기존에 사용한 문장 BACKUP + --SELECT DATEDIFF( + -- MINUTE, + -- CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')), + -- ISNULL(A.WORK_START_DT, GETDATE()) + -- ) AS LATE_MIN, + -- U.USR_NM, + -- U.TEAM_CD, + -- U.DUTY_CD, + -- A.WORK_START_DT + -- FROM SX_GW0050 A --일별근무계획 + -- JOIN SX_GW0010 U --사용자 + -- ON(1=1 + -- AND U.CORP_NO = A.CORP_NO + -- AND U.USR_ID = A.USR_ID + -- AND (NULLIF(U.DISML_DATE, '') IS NULL OR U.DISML_DATE >= @DISML_DATE) --해고일자 + -- AND (NULLIF(U.RETIREMENT_DATE, '') IS NULL OR U.RETIREMENT_DATE >= @RETIREMENT_DATE) --퇴직일자 + -- ) + -- JOIN SX_CO0070 B --공통근무코드 + -- ON(1=1 + -- AND B.CORP_NO = A.CORP_NO + -- AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + -- AND NULLIF(B.GOTOWORK_TM_NM, '') IS NOT NULL + -- ) + -- WHERE 1=1 + -- AND A.CORP_NO = @CORP_NO + -- AND A.WORK_PLAN_YYMMDD = @WORK_PLAN_YYMMDD + -- AND ( + -- ( + -- 1=1 + -- AND SUBSTRING(B.GOTOWORK_TM_NM, 1, 1) != 'T' + -- AND CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) + -- < ISNULL(A.WORK_START_DT, GETDATE()) + -- ) + -- ) + -- ORDER BY + -- LATE_MIN DESC, + -- USR_NM + + + +END --PROCEDURE END + +GO +/****** Object: StoredProcedure [dbo].[pr_skybridge_gw_today_late_YN_select_ORI] Script Date: 2026-03-30 오후 1:12:19 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +-- ======================================================================================= +-- Author : 박종응 +-- Create date : 2017-01-17 +-- Create Description : 스카이브릿지 그룹웨어 출근체크 클릭시 지작인지를 체크 +-- Update date : +-- Update Description : + +-- exec pr_skybridge_gw_today_late_YN_select '1111', '20170307' , '10352' +-- exec pr_skybridge_gw_today_late_YN_select '1111', '20170307' , '00026' +-- exec pr_skybridge_gw_today_late_YN_select '1111', '20170307' , '00001' + + +-- ======================================================================================= +CREATE PROCEDURE [dbo].[pr_skybridge_gw_today_late_YN_select_ORI] + + @CORP_NO VARCHAR(15) --회사코드 + , @WORK_PLAN_YYMMDD VARCHAR(8) --근무일자(계획) + , @USR_ID VARCHAR(20) --사용자 + +AS + + +BEGIN + + SET XACT_ABORT ON + SET NOCOUNT ON + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + + + SELECT COUNT('A') AS LATE_YN --1이면 지각 + FROM ( + SELECT LATE_MIN + , USR_NM + , TEAM_CD + , DUTY_CD + , WORK_START_DT + , USR_ID + + FROM ( + SELECT DATEDIFF( + MINUTE, + CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')), + ISNULL(A.WORK_START_DT, GETDATE()) + ) AS LATE_MIN, + U.USR_NM, + U.TEAM_CD, + U.DUTY_CD, + A.WORK_START_DT, + U.USR_ID + FROM SX_GW0050 A --일별근무계획 + JOIN SX_GW0010 U --사용자 + ON(1=1 + AND U.CORP_NO = A.CORP_NO + AND U.USR_ID = A.USR_ID + ) + JOIN SX_CO0070 B --공통근무코드 + ON(1=1 + AND B.CORP_NO = A.CORP_NO + AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + AND NULLIF(B.GOTOWORK_TM_NM, '') IS NOT NULL + ) + WHERE 1=1 + AND A.CORP_NO = @CORP_NO + AND A.WORK_PLAN_YYMMDD = @WORK_PLAN_YYMMDD + AND ISNULL(A.WORK_CD, A.PLAN_WORK_CD) NOT IN('N', 'Z') + AND ( + ( + 1=1 + AND DATEPART(DW, @WORK_PLAN_YYMMDD) NOT IN('1', '7') + AND SUBSTRING(B.GOTOWORK_TM_NM, 1, 1) != 'T' + AND CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) + < ISNULL(A.WORK_START_DT, GETDATE()) + ) + ) + ) AS TA + WHERE 1=1 + --AND TA.WORK_START_DT IS NOT NULL + + ) TA + WHERE USR_ID = @USR_ID + AND LATE_MIN >= 1 + + + + + -- --기존에 사용한 문장 BACKUP + --SELECT DATEDIFF( + -- MINUTE, + -- CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')), + -- ISNULL(A.WORK_START_DT, GETDATE()) + -- ) AS LATE_MIN, + -- U.USR_NM, + -- U.TEAM_CD, + -- U.DUTY_CD, + -- A.WORK_START_DT + -- FROM SX_GW0050 A --일별근무계획 + -- JOIN SX_GW0010 U --사용자 + -- ON(1=1 + -- AND U.CORP_NO = A.CORP_NO + -- AND U.USR_ID = A.USR_ID + -- AND (NULLIF(U.DISML_DATE, '') IS NULL OR U.DISML_DATE >= @DISML_DATE) --해고일자 + -- AND (NULLIF(U.RETIREMENT_DATE, '') IS NULL OR U.RETIREMENT_DATE >= @RETIREMENT_DATE) --퇴직일자 + -- ) + -- JOIN SX_CO0070 B --공통근무코드 + -- ON(1=1 + -- AND B.CORP_NO = A.CORP_NO + -- AND B.WORK_CD = ISNULL(NULLIF(A.WORK_CD, ''), A.PLAN_WORK_CD) + -- AND NULLIF(B.GOTOWORK_TM_NM, '') IS NOT NULL + -- ) + -- WHERE 1=1 + -- AND A.CORP_NO = @CORP_NO + -- AND A.WORK_PLAN_YYMMDD = @WORK_PLAN_YYMMDD + -- AND ( + -- ( + -- 1=1 + -- AND SUBSTRING(B.GOTOWORK_TM_NM, 1, 1) != 'T' + -- AND CONVERT(DATETIME, STUFF(STUFF(STUFF(CONCAT(A.WORK_PLAN_YYMMDD, B.GOTOWORK_TM_NM, '00'), 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) + -- < ISNULL(A.WORK_START_DT, GETDATE()) + -- ) + -- ) + -- ORDER BY + -- LATE_MIN DESC, + -- USR_NM + + + +END --PROCEDURE END + +GO +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'정렬순서' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SX_GW0050', @level2type=N'COLUMN',@level2name=N'SORT_ODR' +GO +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'정렬순서' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SX_GW0060', @level2type=N'COLUMN',@level2name=N'SORT_ODR' +GO +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'회사번호' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SX_GW0100', @level2type=N'COLUMN',@level2name=N'CORP_NO' +GO +USE [master] +GO +ALTER DATABASE [logins_test] SET READ_WRITE +GO diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..93827e9 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,96 @@ +pipeline { + agent any + + environment { + DOCKER_REGISTRY = "${env.DOCKER_REGISTRY ?: 'localhost:5000'}" + IMAGE_BACKEND = "${DOCKER_REGISTRY}/gw-backend" + IMAGE_FRONTEND = "${DOCKER_REGISTRY}/gw-frontend" + GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + IMAGE_TAG = "${env.BUILD_NUMBER}-${GIT_COMMIT_SHORT}" + } + + options { + buildDiscarder(logRotator(numToKeepStr: '10')) + timeout(time: 30, unit: 'MINUTES') + } + + stages { + + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Backend Build') { + steps { + dir('NEW/backend') { + sh './gradlew clean build -x test --no-daemon' + } + } + } + + stage('Frontend Build') { + steps { + dir('NEW/frontend') { + sh 'pnpm install --frozen-lockfile' + sh 'pnpm build' + } + } + } + + stage('Docker Build & Push') { + parallel { + stage('Backend Image') { + steps { + dir('NEW/backend') { + sh """ + docker build -t ${IMAGE_BACKEND}:${IMAGE_TAG} -t ${IMAGE_BACKEND}:latest . + docker push ${IMAGE_BACKEND}:${IMAGE_TAG} + docker push ${IMAGE_BACKEND}:latest + """ + } + } + } + stage('Frontend Image') { + steps { + dir('NEW/frontend') { + sh """ + docker build -t ${IMAGE_FRONTEND}:${IMAGE_TAG} -t ${IMAGE_FRONTEND}:latest . + docker push ${IMAGE_FRONTEND}:${IMAGE_TAG} + docker push ${IMAGE_FRONTEND}:latest + """ + } + } + } + } + } + + stage('Deploy') { + when { + branch 'main' + } + steps { + dir('NEW') { + sh """ + docker-compose pull + docker-compose up -d --no-build + docker-compose ps + """ + } + } + } + } + + post { + success { + echo "Build ${IMAGE_TAG} deployed successfully." + } + failure { + echo "Build failed. Check logs." + } + always { + sh 'docker system prune -f --filter "until=24h" || true' + } + } +} diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..ebc2a75 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,339 @@ +# 그룹웨어 마이그레이션 가이드 + +## 현행 시스템 → Spring Boot + Next.js 전환 + +--- + +## 현행 시스템 개요 + +| 항목 | 내용 | +|------|------| +| 프레임워크 | Grails 2.5.0 | +| 언어 | Groovy / Java | +| ORM | MyBatis (SQL XML 방식) | +| DB | Oracle (JNDI: `java:comp/env/loginsDS`) | +| 빌드 | Maven | +| 뷰 | GSP + Daum Editor | +| 버전관리 | SVN | + +### 현행 주요 모듈 + +| 모듈 | 설명 | +|------|------| +| board | 게시판 | +| tam | 결재 관리 | +| wplan | 근무 계획 | +| wtime | 근무 시간 관리 | +| envset | 환경 설정 / 사용자 관리 | +| fedex | 배송 연동 | +| common | 공통 (인증, 코드, 첨부파일, 보안) | + +--- + +## 신규 기술 스택 (회사 표준) + +| 계층 | 기술 | 버전 | +|------|------|------| +| OS | Ubuntu | 24.04 LTS | +| Web/Proxy | Nginx | 1.26.x Stable | +| Frontend | Next.js | 15.x (React 19 기반) | +| Runtime | Node.js | 22 LTS | +| 패키지 관리 | pnpm | 최신 | +| API Gateway | Spring Cloud Gateway | 4.x (Spring Boot 3.x 기반) | +| Backend | Spring Boot | 3.4.x | +| JDK | OpenJDK | 21 LTS | +| 빌드 도구 | Gradle | 8.x | +| DB (신규 표준) | MariaDB | 11.4 LTS | +| DB (현재 유지) | Oracle | 기존 그대로 유지 | +| Cache | Redis | 7.x | +| MQ | RabbitMQ | 3.13.x | +| 컨테이너 | Docker / Docker Compose | 최신 Stable | +| 형상관리 | GitHub | - | +| CI/CD | Jenkins | LTS | + +--- + +## 신규 아키텍처 + +``` +[Nginx] + │ +[Next.js 15.x] ──→ [Spring Cloud Gateway 4.x] ──→ [Spring Boot 3.4.x] + │ + [Redis Cache] + │ + [Oracle DB] ← 현재 + [MariaDB] ← 추후 전환 +``` + +### 폴더 구조 + +``` +NEW/ + backend/ ← Spring Boot 3.4.x 프로젝트 (Gradle) + frontend/ ← Next.js 15.x 프로젝트 (pnpm) + MIGRATION_GUIDE.md +``` + +--- + +## DB 전환 전략 + +### 단계별 계획 + +``` +1단계 (현재): Oracle 유지하며 신규 시스템 개발 +2단계 (추후): 신규 시스템 안정화 및 검증 완료 후 MariaDB 마이그레이션 +``` + +### SQL 이중화 전략 (MyBatis databaseId 활용) + +기존 Oracle SQL XML을 그대로 사용하면서, MariaDB 문법 버전을 병행 작성. +전환 시 `application.yml`의 databaseId 설정만 변경하면 즉시 교체 가능. + +```xml + + + + + +``` + +### Oracle → MariaDB 주요 변환 포인트 + +| 항목 | Oracle | MariaDB | +|------|--------|---------| +| 페이징 | ROWNUM | LIMIT / OFFSET | +| 시퀀스 | sequence.NEXTVAL | AUTO_INCREMENT | +| 날짜 함수 | SYSDATE, TO_DATE | NOW(), STR_TO_DATE | +| 문자열 연결 | `||` | CONCAT() 또는 `||` | +| NVL | NVL() | IFNULL() / COALESCE() | +| DECODE | DECODE() | CASE WHEN | +| 가상 테이블 | FROM DUAL | FROM DUAL (지원) 또는 생략 | + +--- + +## Phase 1. 분석 및 설계 + +### 1-1. 현행 파악 체크리스트 + +- [ ] 각 컨트롤러의 URL 매핑 목록화 (`UrlMappings.groovy` + 각 컨트롤러) +- [ ] 세션/인증 방식 파악 (`SecurityFilter`, `AuthService`) +- [ ] 공통 유틸 파악 (`CommonUtil`, `ReqUtil`, `FormatUtil` 등) +- [ ] SQL XML의 쿼리별 입출력 파라미터 파악 +- [ ] 파일 첨부 저장 방식 파악 (`AttachService`) +- [ ] 메뉴/권한 구조 파악 (`MenuService`, `SecurityService`) + +### 1-2. 인증 구조 전환 설계 + +``` +기존: 세션 기반 (SecurityFilter.groovy) +신규: JWT 기반 + - POST /api/auth/login → JWT 발급 + - 모든 요청 헤더: Authorization: Bearer {token} + - Spring Security Filter Chain 구성 + - Redis를 활용한 토큰 블랙리스트/Refresh Token 관리 +``` + +--- + +## Phase 2. Spring Boot 백엔드 구성 + +### 2-1. 주요 의존성 (build.gradle) + +```groovy +dependencies { + // Web + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Security & JWT + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt-api:0.12.x' + + // MyBatis + implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.x' + + // Oracle JDBC + implementation 'com.oracle.database.jdbc:ojdbc11' + + // MariaDB JDBC (추후 전환용) + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // Excel (Apache POI) + implementation 'org.apache.poi:poi-ooxml:5.x' +} +``` + +### 2-2. MyBatis 마이그레이션 (기존 SQL XML 재사용) + +기존 `src/java/sql/` 의 XML 파일을 Spring Boot 프로젝트로 복사 후 경미한 수정만 필요. + +| 작업 | 내용 | +|------|------| +| Mapper XML 이동 | `src/main/resources/mapper/` 로 복사 | +| Mapper Interface 생성 | XML의 각 쿼리 ID에 대응하는 Java Interface 작성 | +| VO/DTO 클래스 | Groovy VO → Java POJO (Lombok 활용) | +| SqlSessionFactory | MyBatis Spring Boot Starter 자동 설정 | +| databaseId 설정 | Oracle/MariaDB 구분을 위한 DatabaseIdProvider 등록 | + +### 2-3. 모듈별 변환 순서 + +``` +1. common → 인증(JWT + Redis), 코드, 공통 유틸 +2. envset → 사용자/권한/메뉴 관리 +3. board → 게시판 (전형적인 CRUD로 패턴 확립) +4. tam → 결재 워크플로우 +5. wplan → 근무 계획 +6. wtime → 근무 시간 +7. fedex → 외부 연동 +``` + +### 2-4. API 응답 공통 형식 + +```json +{ + "success": true, + "data": { ... }, + "message": "", + "pagination": { + "page": 1, + "size": 20, + "total": 100 + } +} +``` + +--- + +## Phase 3. Next.js 프론트엔드 구성 + +### 3-1. 주요 의존성 + +``` +- Next.js 15.x (App Router, React 19) +- TypeScript +- Tailwind CSS +- shadcn/ui (UI 컴포넌트) +- TanStack Query v5 (서버 상태 관리) +- Zustand (클라이언트 상태 관리) +- react-hook-form + zod (폼 검증) +- axios (API 호출) +- Toast UI Editor 또는 Tiptap (WYSIWYG - Daum Editor 대체) +``` + +### 3-2. 페이지 구조 + +``` +/app + /login + /(main) + /dashboard + /board/[boardType] + /tam ← 결재 + /wplan ← 근무계획 + /wtime ← 근무시간 + /envset ← 환경설정 + /users + /codes + /menus +``` + +### 3-3. GSP → Next.js 화면 변환 + +``` +Daum Editor → Toast UI Editor 또는 Tiptap +GSP 페이징 태그 → 커스텀 Pagination 컴포넌트 +GSP 공통 레이아웃 → app/layout.tsx +세션 체크 → JWT 기반 Next.js Middleware +``` + +--- + +## Phase 4. 인프라 구성 (Docker) + +### docker-compose.yml 구성 예시 + +```yaml +services: + nginx: + image: nginx:1.26 + frontend: + build: ./frontend # Next.js + gateway: + build: ./gateway # Spring Cloud Gateway + backend: + build: ./backend # Spring Boot + redis: + image: redis:7 + rabbitmq: + image: rabbitmq:3.13-management +``` + +--- + +## Phase 5. 통합 및 검증 + +### 5-1. 모듈별 검증 체크리스트 + +- [ ] 기존 SQL 결과와 신규 API 응답값 동일 여부 확인 +- [ ] 페이징, 검색 조건 동작 확인 +- [ ] 파일 첨부/다운로드 동작 확인 +- [ ] 결재 워크플로우 흐름 검증 +- [ ] 권한/메뉴 제어 동작 확인 +- [ ] Excel 다운로드 동작 확인 + +### 5-2. 병행 운영 전략 + +``` +1단계: 기존 Grails 운영 유지 +2단계: 신규 시스템 완성 모듈부터 부분 교체 +3단계: 전체 전환 후 기존 시스템 종료 +4단계: DB를 Oracle → MariaDB 마이그레이션 +``` + +--- + +## 작업량 예상 + +| 영역 | 난이도 | 이유 | +|------|--------|------| +| MyBatis SQL 재사용 | 낮음 | XML 거의 그대로 사용 가능 | +| SQL 이중화 (Oracle+MariaDB) | 중간 | databaseId로 병행 관리 | +| Spring Boot API 변환 | 중간 | 서비스 로직 Groovy → Java 변환 | +| JWT + Redis 인증 | 중간 | 세션 → Stateless 구조 변경 | +| 결재(tam) 로직 | 높음 | 워크플로우 복잡도 | +| Next.js 화면 재작성 | 높음 | 전체 UI 새로 작성 필요 | +| 파일 첨부 처리 | 중간 | 저장 경로/방식 재설계 필요 | +| Docker 인프라 구성 | 중간 | 서비스별 컨테이너화 | + +--- + +## 진행 현황 + +- [x] NEW 폴더 및 하위 구조 생성 +- [x] 마이그레이션 가이드 문서 작성 (기술 스택 확정) +- [ ] Spring Boot 백엔드 프로젝트 초기 세팅 +- [ ] Next.js 프론트엔드 프로젝트 초기 세팅 +- [ ] Phase 1: 현행 분석 +- [ ] Phase 2: 백엔드 구축 +- [ ] Phase 3: 프론트엔드 구축 +- [ ] Phase 4: 인프라(Docker) 구성 +- [ ] Phase 5: 통합 및 검증 +- [ ] DB 마이그레이션 (Oracle → MariaDB) diff --git a/TASK_LIST.md b/TASK_LIST.md new file mode 100644 index 0000000..033f33d --- /dev/null +++ b/TASK_LIST.md @@ -0,0 +1,334 @@ +# 그룹웨어 마이그레이션 작업 목록 + +> **범례**: ✅ 완료 | 🔄 진행중 | ⬜ 미착수 +> **원칙**: 각 모듈은 백엔드(SQL → Mapper → Service → Controller) → 프론트엔드(API → Types → Page) 순서로 진행 + +--- + +## 0. 프로젝트 기반 구성 + +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 0-1 | NEW 폴더 구조 생성 | ✅ | backend/, frontend/ | +| 0-2 | MIGRATION_GUIDE.md 작성 | ✅ | DB 전략, 기술 스택 확정 | +| 0-3 | Spring Boot 프로젝트 초기 세팅 | ✅ | Gradle, JDK 21, Spring Boot 3.4.x | +| 0-4 | Next.js 프로젝트 초기 세팅 | ✅ | pnpm, TypeScript, Tailwind | +| 0-5 | DB 설정 (Oracle → MS SQL Server 수정) | ✅ | mssql-jdbc, application-mssql.yml | +| 0-6 | docker-compose.yml 초안 작성 | ✅ | Nginx, Backend, Frontend, Redis, RabbitMQ | +| 0-7 | Nginx 설정 파일 작성 | ✅ | nginx/nginx.conf (API 프록시 + 파일 서빙) | +| 0-8 | Dockerfile 최종 검증 (backend/frontend) | ✅ | backend/Dockerfile, frontend/Dockerfile 존재 | +| 0-9 | GitHub 레포지토리 연결 | ⬜ | .gitignore, 초기 커밋 | +| 0-10 | Jenkins CI/CD 파이프라인 작성 | ✅ | Jenkinsfile (parallel build + deploy) | + +--- + +## 1. 공통 기반 (Common) + +### 1-1. 백엔드 공통 +| # | 작업 | 상태 | 대상 파일 (원본) | +|---|------|------|------| +| 1-1-1 | JWT 유틸 (corpNo 포함) | ✅ | JwtUtil.java | +| 1-1-2 | JWT 인증 필터 | ✅ | JwtAuthenticationFilter.java | +| 1-1-3 | Spring Security 설정 | ✅ | SecurityConfig.java | +| 1-1-4 | Redis 설정 | ✅ | RedisConfig.java | +| 1-1-5 | MyBatis 설정 (DatabaseId 포함) | ✅ | MyBatisConfig.java | +| 1-1-6 | 공통 응답 포맷 (ApiResponse) | ✅ | ApiResponse.java | +| 1-1-7 | 공통 예외 처리 | ✅ | GlobalExceptionHandler.java, BizException.java | +| 1-1-8 | CurrentUser 유틸 + SecurityUtil | ✅ | CurrentUser.java, SecurityUtil.java | +| 1-1-9 | 시퀀스 서비스 | ✅ | SequenceService.java (SX_CO0060) | +| 1-1-10 | 파일 첨부 서비스 | ✅ | AttachService.java, AttachController.java | +| 1-1-11 | 코드 조회 서비스 (공통코드 캐시) | ✅ | CodeService.java (@Cacheable Redis, TTL 6h) | +| 1-1-12 | 이메일 서비스 | ⬜ | EmailService.groovy → EmailService.java | +| 1-1-13 | 공통 SQL XML 이식 | ✅ | common_sql.xml (시퀀스) | +| 1-1-14 | attach SQL XML 이식 | ✅ | attach_sql.xml (MSSQL + MariaDB) | +| 1-1-15 | code SQL XML 이식 | ✅ | code_sql.xml (getCodeList, searchUser, getWorkCdList) | + +### 1-2. 프론트엔드 공통 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 1-2-1 | Axios 클라이언트 + 401 자동 갱신 | ✅ | lib/api/client.ts | +| 1-2-2 | Zustand 인증 스토어 | ✅ | lib/store/authStore.ts | +| 1-2-3 | TanStack Query Provider | ✅ | components/common/Providers.tsx | +| 1-2-4 | JWT 미들웨어 (라우트 보호) | ✅ | middleware.ts | +| 1-2-5 | 공통 레이아웃 (Sidebar + Header) | ✅ | components/layout/ | +| 1-2-6 | 공통 타입 정의 | ✅ | types/common.ts, types/auth.ts | +| 1-2-7 | 공통 UI 컴포넌트 (Modal, Pagination, Table) | ✅ | Modal.tsx, Pagination.tsx 완성 | +| 1-2-8 | 파일 첨부 컴포넌트 | ✅ | components/common/FileUpload.tsx | +| 1-2-9 | Toast 알림 컴포넌트 | ✅ | Toast.tsx (ToastProvider + useToast hook) | +| 1-2-10 | 날짜 선택 컴포넌트 | ⬜ | components/common/DatePicker.tsx | + +--- + +## 2. 인증 (Auth / Index) + +> 원본: `IndexController`, `SecurityFilter`, `AuthService`, `SecurityService`, `UserService` + +### 2-1. 백엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 2-1-1 | user_sql.xml 이식 | ✅ | getUserInfo, getUserInfoWithPwd | +| 2-1-2 | security_sql.xml 이식 | ✅ | getUserRoleList | +| 2-1-3 | menu_sql.xml 이식 | ✅ | getMenuList, getControllerRoleList | +| 2-1-4 | UserMapper / SecurityMapper / MenuMapper | ✅ | | +| 2-1-5 | PasswordUtil (SHA-256 + Base64) | ✅ | 기존 DB 비밀번호 호환 | +| 2-1-6 | AuthService (로그인/로그아웃/토큰재발급) | ✅ | Redis RefreshToken | +| 2-1-7 | CustomUserDetailsService | ✅ | | +| 2-1-8 | AuthController (login/logout/refresh/me) | ✅ | | + +### 2-2. 프론트엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 2-2-1 | authApi (login/logout/refresh) | ✅ | lib/api/auth.ts | +| 2-2-2 | 로그인 페이지 + LoginForm | ✅ | app/login/page.tsx | +| 2-2-3 | 권한(MENU_AUTH_CD) 선택 드롭다운 | ✅ | 기존 시스템 동일 | +| 2-2-4 | 로그아웃 처리 (Header) | ✅ | | +| 2-2-5 | 아이디 저장 기능 | ✅ | localStorage gw_save_id/gw_saved_login_id | + +--- + +## 3. 환경설정 (Envset) + +### 3-1. 직원정보 (Envset0010 / SX_GW0010) +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 3-1-1 | envset0010_sql.xml 이식 | ✅ | 페이징 포함 | +| 3-1-2 | UserManageMapper | ✅ | | +| 3-1-3 | UserManageService | ✅ | 시퀀스 ID 생성, PW 암호화 | +| 3-1-4 | UserManageController | ✅ | CRUD REST API | +| 3-1-5 | 직원 목록 페이지 | ✅ | 마스터-디테일 구조 | +| 3-1-6 | 직원 등록/수정 폼 | ✅ | | +| 3-1-7 | 직원 사진 업로드 | ✅ | FileUpload 컴포넌트 연동 (users/page.tsx) | +| 3-1-8 | 근무코드 관리 (Envset0040, SX_CO0070) | ✅ | envset0040_sql.xml + WorkCdMapper/Service/Controller + workcd/page.tsx | + +### 3-2. 공통코드 관리 (Envset0020/0030 / SX_CO0030, SX_CO0040) +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 3-2-1 | envset0020_sql.xml 이식 | ✅ | | +| 3-2-2 | CodeManageMapper / Service / Controller | ✅ | 배치 저장 (I/U/D) | +| 3-2-3 | 코드 관리 페이지 | ✅ | 코드인덱스 + 코드 | + +### 3-3. 메뉴/권한 관리 (Envset0050 / SX_CO0080, SX_CO0090, SX_GW0130) +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 3-3-1 | envset0050_sql.xml 이식 | ✅ | MERGE INTO 포함 | +| 3-3-2 | MenuManageMapper / Service / Controller | ✅ | | +| 3-3-3 | 메뉴 관리 페이지 | ✅ | | +| 3-3-4 | 권한별 메뉴 설정 페이지 | ✅ | 체크박스 토글 | +| 3-3-5 | 사용자 권한 관리 페이지 | ✅ | 사용자 autocomplete | + +--- + +## 4. 게시판 (Board) + +> 원본: `Board0010Controller`, `Board_0001~0008Controller`, `board0010_sql.xml` +> 8개 게시판 유형을 하나의 Controller로 통합 (boardType 파라미터로 구분) + +### 4-1. 백엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 4-1-1 | board0010_sql.xml 분석 및 이식 | ✅ | MSSQL + MariaDB 이중 작성 | +| 4-1-2 | BoardMapper 인터페이스 | ✅ | | +| 4-1-3 | BoardService | ✅ | 목록/상세/등록/수정/삭제/조회수/댓글 | +| 4-1-4 | BoardController | ✅ | `/api/board/{boardType}` REST | +| 4-1-5 | 파일 첨부 연동 | ✅ | AttachService 연동 완료 | + +### 4-2. 프론트엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 4-2-1 | boardApi 정의 | ✅ | lib/api/board.ts | +| 4-2-2 | 게시판 목록 페이지 | ✅ | app/(main)/board/[boardType]/page.tsx | +| 4-2-3 | 게시물 상세 페이지 | ✅ | 댓글 포함 | +| 4-2-4 | 게시물 등록/수정 페이지 | ✅ | PostForm 컴포넌트 | +| 4-2-5 | 파일 첨부 UI | ✅ | FileUpload 컴포넌트 연동 | + +--- + +## 5. 결재 (TAM) + +> 원본: `Tam0010~0040Controller`, `tam0010~0040_sql.xml`, `ApvdocService`, `apvdoc_sql.xml` +> 가장 복잡한 모듈 - 워크플로우 로직 포함 + +### 5-1. 백엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 5-1-1 | tam_sql.xml 분석 및 이식 (0010~0040 통합) | ✅ | MSSQL + MariaDB 이중 작성 | +| 5-1-2 | apvdoc_sql.xml 분석 및 이식 | ✅ | 결재 문서 공통, DECLARE @var → subquery | +| 5-1-3 | TamMapper / ApvdocMapper | ✅ | | +| 5-1-4 | ApvdocService (워크플로우 로직) | ✅ | 결재선, 승인/반려, 상태 변경 | +| 5-1-5 | TamService | ✅ | TAM0010~0040 서비스 | +| 5-1-6 | TamController | ✅ | `/api/tam/` REST | + +### 5-2. 프론트엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 5-2-1 | tamApi 정의 | ✅ | lib/api/tam.ts | +| 5-2-2 | 연차 관리 페이지 (TAM0010) | ✅ | 인라인 편집 | +| 5-2-3 | 결재 신청 목록 페이지 (TAM0020) | ✅ | 상태/종류 필터 | +| 5-2-4 | 결재 신청 등록/상세 페이지 | ✅ | ApvreqForm + ApproverSection 컴포넌트 | +| 5-2-5 | 결재 처리 목록/상세 페이지 (TAM0030) | ✅ | 승인/반려 처리 | +| 5-2-6 | 근태 현황 페이지 (TAM0040) | ✅ | 종류별 집계 | + +--- + +## 6. 근무계획 (Wplan) + +> 원본: `Wplan0010~0030Controller`, `wplan0010~0030_sql.xml` + +### 6-1. 백엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 6-1-1 | wplan_sql.xml 분석 및 이식 (0010~0030 통합) | ✅ | MSSQL + MariaDB 이중 작성 | +| 6-1-2 | WplanMapper / Service / Controller | ✅ | `/api/wplan/` | + +### 6-2. 프론트엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 6-2-1 | wplanApi 정의 | ✅ | lib/api/wplan.ts | +| 6-2-2 | 근무계획 관리 페이지 (Wplan0010) | ✅ | 월별 피벗 테이블 + 인라인 편집 | +| 6-2-3 | 나의근무계획 페이지 (Wplan0020) | ✅ | 월 달력 뷰 | +| 6-2-4 | 전체근무계획 페이지 (Wplan0030) | ✅ | 근무코드 필터 | + +--- + +## 7. 근무시간 (Wtime) + +> 원본: `Wtime0010Controller`, `Wtime0030Controller`, `wtime0010_sql.xml`, `wtime0030_sql.xml`, `work_sql.xml` + +### 7-1. 백엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 7-1-1 | wtime_sql.xml 분석 및 이식 (0010+0030 통합) | ✅ | MSSQL + MariaDB 이중 작성 | +| 7-1-2 | WtimeMapper / Service / Controller | ✅ | `/api/wtime/` | +| 7-1-3 | WorkService (근무시간 재계산 로직) | ⬜ | 추후 필요 시 추가 | + +### 7-2. 프론트엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 7-2-1 | wtimeApi 정의 | ✅ | lib/api/wtime.ts | +| 7-2-2 | 개인별 근무시간 페이지 (Wtime0010) | ✅ | 날짜 범위 검색 | +| 7-2-3 | 월별 근무시간 집계 페이지 (Wtime0030) | ✅ | 개인별 합산 | + +--- + +## 8. FedEx 배송 (Fedex) + +> 원본: `Fedex0010Controller`, `fedex0010_sql.xml` + +### 8-1. 백엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 8-1-1 | fedex0010_sql.xml 분석 및 이식 | ✅ | MSSQL + MariaDB, errorMng 테이블 | +| 8-1-2 | FedexMapper / Service / Controller | ✅ | `/api/fedex/` | + +### 8-2. 프론트엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 8-2-1 | fedexApi 정의 | ✅ | lib/api/fedex.ts | +| 8-2-2 | FedEx 수입정정 목록 페이지 | ✅ | fedex/0010/page.tsx | +| 8-2-3 | FedEx 수입정정 등록/상세 페이지 | ✅ | write + [sq] 페이지 | + +--- + +## 9. 메인/대시보드 (Main) + +> 원본: `Main0010Controller`, `Main0020Controller`, `main0010_sql.xml`, `main0020_sql.xml` + +### 9-1. 백엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 9-1-1 | main0010_sql.xml 분석 및 이식 | ✅ | main_sql.xml (직접 SQL, stored proc 불사용) | +| 9-1-2 | main0020_sql.xml 분석 및 이식 | ✅ | main_sql.xml 통합 | +| 9-1-3 | MainMapper / Service / Controller | ✅ | `/api/main/dashboard` | + +### 9-2. 프론트엔드 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 9-2-1 | mainApi 정의 | ✅ | lib/api/main.ts | +| 9-2-2 | 대시보드 페이지 | ✅ | 결재 대기 배너, 공지/게시판/근무 위젯 | +| 9-2-3 | 메인 레이아웃 완성 | ✅ | Sidebar 동적 메뉴 | + +--- + +## 10. 공통 기능 (파일첨부 / 이메일) + +### 10-1. 파일 첨부 (AttachService) +> 원본: `AttachController`, `AttachService`, `attach_sql.xml` +> 모든 모듈(게시판, 결재, 직원사진)에서 사용 + +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 10-1-1 | attach_sql.xml 분석 및 이식 | ✅ | attach_sql.xml (MSSQL + MariaDB) | +| 10-1-2 | AttachMapper | ✅ | AttachMapper.java | +| 10-1-3 | AttachService (업로드/다운로드/삭제) | ✅ | 로컬 저장소, 확장자 검증 | +| 10-1-4 | AttachController | ✅ | `/api/attach/` Multipart + download | +| 10-1-5 | FileUpload 컴포넌트 | ✅ | FileUpload.tsx (드래그앤드롭) | +| 10-1-6 | 파일 다운로드 처리 | ✅ | Content-Disposition URLEncoder | + +### 10-2. 이메일 서비스 +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 10-2-1 | EmailService 이식 | ⬜ | Spring Mail (SMTP) | +| 10-2-2 | 결재 알림 이메일 연동 | ⬜ | TAM 모듈 완성 후 | + +--- + +## 11. 인프라 및 배포 + +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 11-1 | Nginx 설정 (nginx.conf) | ✅ | API 프록시 + 파일 서빙 | +| 11-2 | docker-compose.yml 최종 완성 | ✅ | env_file, healthcheck, volumes | +| 11-3 | .env 파일 구성 | ✅ | DB, JWT, Redis, RabbitMQ | +| 11-4 | Jenkins Jenkinsfile 작성 | ✅ | parallel build + deploy | +| 11-5 | Spring Boot actuator 헬스체크 | ✅ | /actuator/health (SecurityConfig 허용) | +| 11-6 | 로그 설정 (logback-spring.xml) | ✅ | 프로파일별 콘솔/파일 분리 | + +--- + +## 12. DB 마이그레이션 준비 (SQL Server → MariaDB) + +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 12-1 | 전체 SQL XML MariaDB 버전 작성 | ✅ | 전체 17개 XML 파일 완료 | +| 12-2 | GETDATE() → NOW() 변환 | ✅ | 모든 DML 문 MariaDB 버전에 반영 | +| 12-3 | ISNULL() → IFNULL() 변환 | ✅ | MariaDB databaseId 버전에 반영 | +| 12-4 | MERGE INTO → INSERT ON DUPLICATE KEY | ✅ | common_sql, envset0050_sql 반영 | +| 12-5 | SELECT TOP N → LIMIT N 변환 | ✅ | MariaDB 버전에 LIMIT 적용 | +| 12-6 | OFFSET...FETCH NEXT → LIMIT...OFFSET 변환 | ✅ | 페이징 쿼리 전체 반영 | +| 12-7 | 데이터 마이그레이션 스크립트 작성 | ⬜ | SQL Server → MariaDB | +| 12-8 | application-mariadb.yml 완성 | ⬜ | | +| 12-9 | MariaDB 환경에서 전체 기능 검증 | ⬜ | | + +--- + +## 13. 검증 및 완료 + +| # | 작업 | 상태 | 비고 | +|---|------|------|------| +| 13-1 | API 전체 목록 문서화 (Swagger) | ⬜ | springdoc-openapi | +| 13-2 | 기존 시스템 대비 기능 동등성 검증 | ⬜ | 모듈별 체크리스트 | +| 13-3 | 성능 테스트 (페이징, 대용량 조회) | ⬜ | | +| 13-4 | 보안 점검 (SQL Injection, XSS, CSRF) | ⬜ | | +| 13-5 | 병행 운영 계획 수립 | ⬜ | 기존 Grails 유지 기간 결정 | +| 13-6 | 운영 전환 (Cut-over) | ⬜ | | + +--- + +## 진행 현황 요약 + +``` +전체 작업 수: 약 130개 +완료: 약 40개 (31%) +진행중: 0개 +미착수: 약 90개 (69%) +``` + +### 권장 다음 작업 순서 +1. **1-1-10** 파일 첨부 서비스 → 게시판, 결재, 직원사진 모두 필요 +2. **4** 게시판 → 가장 단순한 CRUD, 패턴 확립용 +3. **5** 결재 → 핵심 업무 기능 (복잡도 높음) +4. **6, 7** 근무계획/근무시간 +5. **8** FedEx +6. **9** 대시보드 +7. **11** 인프라 +8. **12** MariaDB 전환 준비 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..0af0130 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,10 @@ +.gradle/ +build/ +*.class +*.jar +!gradle/wrapper/gradle-wrapper.jar +.idea/ +*.iml +out/ +.env +application-local.yml diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..29cba75 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:21-jdk-slim AS builder +WORKDIR /app +COPY . . +RUN ./gradlew bootJar -x test + +FROM openjdk:21-jdk-slim +WORKDIR /app +COPY --from=builder /app/build/libs/*.jar app.jar +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 0000000..17a711c --- /dev/null +++ b/backend/build.gradle @@ -0,0 +1,92 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.1' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.company' +version = '1.0.0-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', '2024.0.0') + set('mybatisVersion', '3.0.4') + set('jjwtVersion', '0.12.6') +} + +dependencies { + // Web + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Actuator (헬스체크) + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JWT + implementation "io.jsonwebtoken:jjwt-api:${jjwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}" + + // MyBatis + implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisVersion}" + + // MS SQL Server JDBC (현재 사용) - 9.4.1: encrypt=false 시 TLS 미사용 (TLS1.0 서버 호환) + runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc:9.4.1.jre11' + + // MariaDB JDBC (추후 전환용) + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // RabbitMQ + implementation 'org.springframework.boot:spring-boot-starter-amqp' + + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // Excel (Apache POI) + implementation 'org.apache.poi:poi-ooxml:5.3.0' + + // Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4' + +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +bootRun { + args '--spring.profiles.active=mssql,local' + jvmArgs "-Djava.security.properties=${projectDir}/tls-local.security" +} diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8bdaf60c75ab801e22807dde59e12a8735a34077 GIT binary patch literal 45457 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 c82=K%QXLKFx%@O{wJjEi6Y56o#$)Bpeg literal 0 HcmV?d00001 diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2a84e18 --- /dev/null +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew new file mode 100644 index 0000000..ef07e01 --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/settings.gradle b/backend/settings.gradle new file mode 100644 index 0000000..4c96eb9 --- /dev/null +++ b/backend/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'gw-backend' diff --git a/backend/src/main/java/com/company/gw/GwBackendApplication.java b/backend/src/main/java/com/company/gw/GwBackendApplication.java new file mode 100644 index 0000000..68e0627 --- /dev/null +++ b/backend/src/main/java/com/company/gw/GwBackendApplication.java @@ -0,0 +1,16 @@ +package com.company.gw; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +@EnableCaching +public class GwBackendApplication { + + public static void main(String[] args) { + SpringApplication.run(GwBackendApplication.class, args); + } +} diff --git a/backend/src/main/java/com/company/gw/board/controller/BoardController.java b/backend/src/main/java/com/company/gw/board/controller/BoardController.java new file mode 100644 index 0000000..c926404 --- /dev/null +++ b/backend/src/main/java/com/company/gw/board/controller/BoardController.java @@ -0,0 +1,115 @@ +package com.company.gw.board.controller; + +import com.company.gw.board.service.BoardService; +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/board") +@RequiredArgsConstructor +public class BoardController { + + private final BoardService boardService; + + /** 게시물 목록 */ + @GetMapping("/{untyBbsCd}") + public ApiResponse> getPostList( + @PathVariable String untyBbsCd, + @RequestParam(defaultValue = "") String searchText, + @RequestParam(defaultValue = "1") int pageNo, + @RequestParam(defaultValue = "20") int pageSize, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok( + boardService.getPostList(currentUser.getCorpNo(), untyBbsCd, searchText, pageNo, pageSize)); + } + + /** 게시물 상세 (조회수 증가) */ + @GetMapping("/{untyBbsCd}/{untyBbsSno}") + public ApiResponse> getPostInfo( + @PathVariable String untyBbsCd, + @PathVariable Long untyBbsSno, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok( + boardService.getPostInfoForView(currentUser.getCorpNo(), untyBbsCd, untyBbsSno)); + } + + /** 게시물 상세 (수정용, 조회수 미증가) */ + @GetMapping("/{untyBbsCd}/{untyBbsSno}/edit") + public ApiResponse> getPostInfoForEdit( + @PathVariable String untyBbsCd, + @PathVariable Long untyBbsSno, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok( + boardService.getPostInfoForEdit(currentUser.getCorpNo(), untyBbsCd, untyBbsSno)); + } + + /** 게시물 등록 */ + @PostMapping("/{untyBbsCd}") + public ApiResponse> insertPost( + @PathVariable String untyBbsCd, + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + body.put("untyBbsCd", untyBbsCd); + return ApiResponse.ok(boardService.insertPost(currentUser.getCorpNo(), body)); + } + + /** 게시물 수정 */ + @PutMapping("/{untyBbsCd}/{untyBbsSno}") + public ApiResponse updatePost( + @PathVariable String untyBbsCd, + @PathVariable Long untyBbsSno, + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + body.put("untyBbsCd", untyBbsCd); + boardService.updatePost(currentUser.getCorpNo(), untyBbsSno, body); + return ApiResponse.ok(null); + } + + /** 게시물 삭제 */ + @DeleteMapping("/{untyBbsCd}/{untyBbsSno}") + public ApiResponse deletePost( + @PathVariable String untyBbsCd, + @PathVariable Long untyBbsSno, + @AuthenticationPrincipal CurrentUser currentUser) { + boardService.deletePost(currentUser.getCorpNo(), untyBbsCd, untyBbsSno); + return ApiResponse.ok(null); + } + + /** 댓글 목록 */ + @GetMapping("/{untyBbsCd}/{untyBbsSno}/comments") + public ApiResponse>> getCommentList( + @PathVariable String untyBbsCd, + @PathVariable Long untyBbsSno, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok( + boardService.getCommentList(currentUser.getCorpNo(), untyBbsCd, untyBbsSno)); + } + + /** 댓글 등록 */ + @PostMapping("/{untyBbsCd}/{untyBbsSno}/comments") + public ApiResponse insertComment( + @PathVariable String untyBbsCd, + @PathVariable Long untyBbsSno, + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + boardService.insertComment(currentUser.getCorpNo(), untyBbsCd, untyBbsSno, body.get("cmmtCn")); + return ApiResponse.ok(null); + } + + /** 댓글 삭제 */ + @DeleteMapping("/{untyBbsCd}/{untyBbsSno}/comments/{cmmtSno}") + public ApiResponse deleteComment( + @PathVariable String untyBbsCd, + @PathVariable Long untyBbsSno, + @PathVariable Long cmmtSno, + @AuthenticationPrincipal CurrentUser currentUser) { + boardService.deleteComment(currentUser.getCorpNo(), untyBbsCd, untyBbsSno, cmmtSno); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/company/gw/board/mapper/BoardMapper.java b/backend/src/main/java/com/company/gw/board/mapper/BoardMapper.java new file mode 100644 index 0000000..9932e0a --- /dev/null +++ b/backend/src/main/java/com/company/gw/board/mapper/BoardMapper.java @@ -0,0 +1,40 @@ +package com.company.gw.board.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface BoardMapper { + + List> getPostList(Map param); + + long getPostListCount(Map param); + + Map getPostInfo(Map param); + + void insertPost(Map param); + + void updatePost(Map param); + + void increaseInqrCnt(Map param); + + void updateEtcAtchNo(Map param); + + void updatePhotoAtchNo(Map param); + + void updateCmmtCnt(Map param); + + void deletePost(Map param); + + List> getCommentList(Map param); + + Map getCommentInfo(Map param); + + void insertComment(Map param); + + void deleteComment(Map param); + + void deleteCommentsByPost(Map param); +} diff --git a/backend/src/main/java/com/company/gw/board/service/BoardService.java b/backend/src/main/java/com/company/gw/board/service/BoardService.java new file mode 100644 index 0000000..0b205a6 --- /dev/null +++ b/backend/src/main/java/com/company/gw/board/service/BoardService.java @@ -0,0 +1,228 @@ +package com.company.gw.board.service; + +import com.company.gw.board.mapper.BoardMapper; +import com.company.gw.common.exception.BizException; +import com.company.gw.common.service.AttachService; +import com.company.gw.common.service.SequenceService; +import com.company.gw.common.util.SecurityUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class BoardService { + + private final BoardMapper boardMapper; + private final SequenceService sequenceService; + private final AttachService attachService; + + // ────────────────────────────────────────────── + // 게시물 목록 + // ────────────────────────────────────────────── + + public Map getPostList(String corpNo, String untyBbsCd, + String searchText, int pageNo, int pageSize) { + if (pageSize > 200) pageSize = 200; + + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("untyBbsCd", untyBbsCd); + param.put("searchText", searchText); + param.put("pageNo", pageNo); + param.put("pageSize", pageSize); + + List> list = boardMapper.getPostList(param); + long total = boardMapper.getPostListCount(param); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + result.put("pageNo", pageNo); + result.put("pageSize", pageSize); + return result; + } + + // ────────────────────────────────────────────── + // 게시물 상세 (조회수 증가 포함) + // ────────────────────────────────────────────── + + @Transactional + public Map getPostInfoForView(String corpNo, String untyBbsCd, Long untyBbsSno) { + Map param = buildPostParam(corpNo, untyBbsCd, untyBbsSno); + + boardMapper.increaseInqrCnt(param); + + Map info = boardMapper.getPostInfo(param); + if (info == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND); + + // 첨부파일 목록 조회 + String etcAtchNo = (String) info.get("ETC_ATCH_NO"); + String photoAtchNo = (String) info.get("PHOTO_ATCH_NO"); + if (etcAtchNo != null) info.put("etcFileList", attachService.getFileList(etcAtchNo)); + if (photoAtchNo != null) info.put("photoFileList", attachService.getFileList(photoAtchNo)); + + return info; + } + + // 수정화면용 (조회수 증가 없음) + @Transactional(readOnly = true) + public Map getPostInfoForEdit(String corpNo, String untyBbsCd, Long untyBbsSno) { + Map info = boardMapper.getPostInfo(buildPostParam(corpNo, untyBbsCd, untyBbsSno)); + if (info == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND); + + String etcAtchNo = (String) info.get("ETC_ATCH_NO"); + String photoAtchNo = (String) info.get("PHOTO_ATCH_NO"); + if (etcAtchNo != null) info.put("etcFileList", attachService.getFileList(etcAtchNo)); + if (photoAtchNo != null) info.put("photoFileList", attachService.getFileList(photoAtchNo)); + + return info; + } + + // ────────────────────────────────────────────── + // 게시물 등록 + // ────────────────────────────────────────────── + + @Transactional + public Map insertPost(String corpNo, Map body) { + String usrId = SecurityUtil.getUsrId(); + Integer sno = Integer.parseInt( + sequenceService.getNextSeqString("SX_GW0020.UNTY_BBS_SNO", 10)); + + Map param = new HashMap<>(body); + param.put("corpNo", corpNo); + param.put("usrId", usrId); + param.put("untyBbsSno", sno); + + boardMapper.insertPost(param); + + // 첨부 확정 + String etcAtchNo = (String) body.get("etcAtchNo"); + String photoAtchNo = (String) body.get("photoAtchNo"); + if (etcAtchNo != null) { + param.put("etcAtchNo", etcAtchNo); + boardMapper.updateEtcAtchNo(param); + attachService.confirmAtchNo(etcAtchNo, "SX_GW0020.ETC_ATCH_NO"); + } + if (photoAtchNo != null) { + param.put("photoAtchNo", photoAtchNo); + boardMapper.updatePhotoAtchNo(param); + attachService.confirmAtchNo(photoAtchNo, "SX_GW0020.PHOTO_ATCH_NO"); + } + + Map result = new HashMap<>(); + result.put("untyBbsSno", sno); + return result; + } + + // ────────────────────────────────────────────── + // 게시물 수정 + // ────────────────────────────────────────────── + + @Transactional + public void updatePost(String corpNo, Long untyBbsSno, Map body) { + String usrId = SecurityUtil.getUsrId(); + Map param = buildPostParam(corpNo, (String) body.get("untyBbsCd"), untyBbsSno); + + Map existing = boardMapper.getPostInfo(param); + if (existing == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND); + if (!usrId.equals(existing.get("CTUSR_ID"))) { + throw new BizException("작성자만 수정할 수 있습니다.", HttpStatus.FORBIDDEN); + } + + param.putAll(body); + param.put("usrId", usrId); + boardMapper.updatePost(param); + + // 첨부 처리 (새 파일이 업로드된 경우) + String etcAtchNo = (String) body.get("etcAtchNo"); + if (etcAtchNo != null) { + param.put("etcAtchNo", etcAtchNo); + boardMapper.updateEtcAtchNo(param); + attachService.confirmAtchNo(etcAtchNo, "SX_GW0020.ETC_ATCH_NO"); + } + } + + // ────────────────────────────────────────────── + // 게시물 삭제 + // ────────────────────────────────────────────── + + @Transactional + public void deletePost(String corpNo, String untyBbsCd, Long untyBbsSno) { + String usrId = SecurityUtil.getUsrId(); + Map param = buildPostParam(corpNo, untyBbsCd, untyBbsSno); + + Map info = boardMapper.getPostInfo(param); + if (info == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND); + if (!usrId.equals(info.get("CTUSR_ID"))) { + throw new BizException("작성자만 삭제할 수 있습니다.", HttpStatus.FORBIDDEN); + } + + boardMapper.deleteCommentsByPost(param); + boardMapper.deletePost(param); + + // 첨부파일 삭제 + String etcAtchNo = (String) info.get("ETC_ATCH_NO"); + String photoAtchNo = (String) info.get("PHOTO_ATCH_NO"); + if (etcAtchNo != null) attachService.deleteFilesByAtchNo(etcAtchNo, true); + if (photoAtchNo != null) attachService.deleteFilesByAtchNo(photoAtchNo, true); + } + + // ────────────────────────────────────────────── + // 댓글 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getCommentList(String corpNo, String untyBbsCd, Long untyBbsSno) { + return boardMapper.getCommentList(buildPostParam(corpNo, untyBbsCd, untyBbsSno)); + } + + @Transactional + public void insertComment(String corpNo, String untyBbsCd, Long untyBbsSno, String cmmtCn) { + String usrId = SecurityUtil.getUsrId(); + Integer cmmtSno = Integer.parseInt( + sequenceService.getNextSeqString("SX_GW0030.CMMT_SNO", 10)); + + Map param = buildPostParam(corpNo, untyBbsCd, untyBbsSno); + param.put("cmmtSno", cmmtSno); + param.put("cmmtCn", cmmtCn); + param.put("usrId", usrId); + + boardMapper.insertComment(param); + boardMapper.updateCmmtCnt(param); + } + + @Transactional + public void deleteComment(String corpNo, String untyBbsCd, Long untyBbsSno, Long cmmtSno) { + String usrId = SecurityUtil.getUsrId(); + Map param = buildPostParam(corpNo, untyBbsCd, untyBbsSno); + param.put("cmmtSno", cmmtSno); + param.put("usrId", usrId); + + Map info = boardMapper.getCommentInfo(param); + if (info == null) throw new BizException("댓글이 존재하지 않습니다.", HttpStatus.NOT_FOUND); + if (!usrId.equals(info.get("CMMT_CTUSR_ID"))) { + throw new BizException("작성자만 삭제할 수 있습니다.", HttpStatus.FORBIDDEN); + } + + boardMapper.deleteComment(param); + boardMapper.updateCmmtCnt(param); + } + + // ────────────────────────────────────────────── + // 내부 유틸 + // ────────────────────────────────────────────── + + private Map buildPostParam(String corpNo, String untyBbsCd, Long untyBbsSno) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("untyBbsCd", untyBbsCd); + p.put("untyBbsSno", untyBbsSno); + return p; + } +} diff --git a/backend/src/main/java/com/company/gw/common/config/JacksonConfig.java b/backend/src/main/java/com/company/gw/common/config/JacksonConfig.java new file mode 100644 index 0000000..83a0d7c --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/config/JacksonConfig.java @@ -0,0 +1,69 @@ +package com.company.gw.common.config; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; +import java.util.Map; + +/** + * MyBatis resultType="map" 은 DB 컬럼명을 그대로 Map 키로 반환합니다 (e.g. WORK_START_DT). + * 프론트엔드는 camelCase(e.g. workStartDt)를 기대하므로, JSON 직렬화 시 변환합니다. + * UPPER_CASE_SNAKE 패턴만 변환하고, 이미 camelCase인 키는 그대로 유지합니다. + */ +@Configuration +public class JacksonConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer mapKeyCamelCaseCustomizer() { + return builder -> { + SimpleModule module = new SimpleModule(); + module.addSerializer(Map.class, new MapCamelCaseSerializer()); + builder.modules(module); + }; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + static class MapCamelCaseSerializer extends JsonSerializer { + @Override + public void serialize(Map map, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + for (Object obj : map.entrySet()) { + Map.Entry entry = (Map.Entry) obj; + String key = entry.getKey() != null ? convertKey(entry.getKey().toString()) : "null"; + gen.writeFieldName(key); + serializers.defaultSerializeValue(entry.getValue(), gen); + } + gen.writeEndObject(); + } + + /** + * UPPER_CASE_SNAKE 패턴인 경우에만 camelCase로 변환. + * 이미 camelCase 이거나 소문자인 키는 그대로 반환. + */ + private String convertKey(String key) { + // 모두 대문자+언더스코어 패턴인 경우만 변환 + if (!key.matches("[A-Z][A-Z0-9_]*")) return key; + + StringBuilder result = new StringBuilder(); + boolean nextUpper = false; + for (int i = 0; i < key.length(); i++) { + char c = key.charAt(i); + if (c == '_') { + nextUpper = true; + } else if (nextUpper) { + result.append(Character.toUpperCase(c)); + nextUpper = false; + } else { + result.append(Character.toLowerCase(c)); + } + } + return result.toString(); + } + } +} diff --git a/backend/src/main/java/com/company/gw/common/config/MyBatisConfig.java b/backend/src/main/java/com/company/gw/common/config/MyBatisConfig.java new file mode 100644 index 0000000..a56bd7d --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/config/MyBatisConfig.java @@ -0,0 +1,53 @@ +package com.company.gw.common.config; + +import org.apache.ibatis.mapping.VendorDatabaseIdProvider; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import javax.sql.DataSource; +import java.util.Properties; + +@Configuration +@MapperScan("com.company.gw.**.mapper") +public class MyBatisConfig { + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); + factoryBean.setDataSource(dataSource); + factoryBean.setMapperLocations( + new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml") + ); + + // databaseId 벤더 매핑 + // SQL Server → "mssql" (기본 구문은 databaseId 없거나 "mssql") + // MySQL → "mariadb" (MariaDB JDBC 2.x가 MySQL로 리포트) + // MariaDB → "mariadb" (MariaDB JDBC 3.x+) + VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); + Properties vendorProps = new Properties(); + vendorProps.setProperty("SQL Server", "mssql"); + vendorProps.setProperty("MySQL", "mariadb"); + vendorProps.setProperty("MariaDB", "mariadb"); + databaseIdProvider.setProperties(vendorProps); + factoryBean.setDatabaseIdProvider(databaseIdProvider); + + // application.yml의 mybatis.configuration이 수동 Bean 생성 시 무시되므로 직접 설정 + org.apache.ibatis.session.Configuration mybatisConfig = new org.apache.ibatis.session.Configuration(); + mybatisConfig.setMapUnderscoreToCamelCase(true); + mybatisConfig.setDefaultFetchSize(100); + mybatisConfig.setDefaultStatementTimeout(30); + factoryBean.setConfiguration(mybatisConfig); + + return factoryBean.getObject(); + } + + @Bean + public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { + return new SqlSessionTemplate(sqlSessionFactory); + } +} diff --git a/backend/src/main/java/com/company/gw/common/config/RedisConfig.java b/backend/src/main/java/com/company/gw/common/config/RedisConfig.java new file mode 100644 index 0000000..eeb72dc --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/config/RedisConfig.java @@ -0,0 +1,61 @@ +package com.company.gw.common.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; +import java.util.Map; + +@Slf4j +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } + + @Bean + public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { + try { + // Redis 연결 가능 여부 확인 + connectionFactory.getConnection().ping(); + + RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(1)) + .serializeKeysWith(RedisSerializationContext.SerializationPair + .fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair + .fromSerializer(new GenericJackson2JsonRedisSerializer())); + + Map cacheConfigs = Map.of( + "code", defaultConfig.entryTtl(Duration.ofHours(6)), + "workCd", defaultConfig.entryTtl(Duration.ofHours(6)) + ); + + log.info("CacheManager: Redis 사용"); + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(defaultConfig) + .withInitialCacheConfigurations(cacheConfigs) + .build(); + + } catch (Exception e) { + log.warn("Redis 연결 실패 → In-Memory CacheManager 사용 ({})", e.getMessage()); + return new ConcurrentMapCacheManager("code", "workCd"); + } + } +} diff --git a/backend/src/main/java/com/company/gw/common/config/SecurityConfig.java b/backend/src/main/java/com/company/gw/common/config/SecurityConfig.java new file mode 100644 index 0000000..f91748f --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/config/SecurityConfig.java @@ -0,0 +1,85 @@ +package com.company.gw.common.config; + +import com.company.gw.common.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/actuator/health").permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(ex -> ex + // 인증 안 된 요청 → 401 (프론트 interceptor가 refresh 시도) + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write("{\"message\":\"Unauthorized\"}"); + }) + // 인증은 됐지만 권한 없음 → 403 + .accessDeniedHandler((request, response, accessDeniedException) -> { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write("{\"message\":\"Forbidden\"}"); + }) + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOriginPatterns(List.of("*")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", config); + return source; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/backend/src/main/java/com/company/gw/common/controller/AttachController.java b/backend/src/main/java/com/company/gw/common/controller/AttachController.java new file mode 100644 index 0000000..87c4a5c --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/controller/AttachController.java @@ -0,0 +1,133 @@ +package com.company.gw.common.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.AttachDto; +import com.company.gw.common.exception.BizException; +import com.company.gw.common.service.AttachService; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.FilenameUtils; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@RestController +@RequestMapping("/api/attach") +@RequiredArgsConstructor +public class AttachController { + + private final AttachService attachService; + + /** 단일 파일 업로드 */ + @PostMapping("/upload") + public ApiResponse upload( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "atchNo", required = false) String atchNo, + @RequestParam(value = "division", required = false) String division) { + return ApiResponse.ok(attachService.uploadFile(file, atchNo, division)); + } + + /** 다중 파일 업로드 */ + @PostMapping("/upload/multi") + public ApiResponse> uploadMulti( + @RequestParam("files") List files, + @RequestParam(value = "atchNo", required = false) String atchNo, + @RequestParam(value = "division", required = false) String division) { + return ApiResponse.ok(attachService.uploadFiles(files, atchNo, division)); + } + + /** 에디터 이미지 업로드 */ + @PostMapping("/upload/image") + public ApiResponse> uploadImage( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "division", required = false) String division) { + return ApiResponse.ok(attachService.uploadEditorImage(file, division)); + } + + /** 첨부번호로 파일 목록 조회 */ + @GetMapping("/list/{atchNo}") + public ApiResponse>> getFileList(@PathVariable String atchNo) { + return ApiResponse.ok(attachService.getFileList(atchNo)); + } + + /** 단일 파일 다운로드 */ + @GetMapping("/download/{atchfileNo}") + public ResponseEntity download(@PathVariable String atchfileNo) { + Map info = attachService.getFileInfo(atchfileNo); + File file = attachService.getPhysicalFile(atchfileNo); + + String fileName = (String) info.get("ATCH_FILE_NM"); + String encodedName; + try { + encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()) + .replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + encodedName = fileName; + } + + String contentType = (String) info.getOrDefault("ATCH_TYPE_NM", + MediaType.APPLICATION_OCTET_STREAM_VALUE); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename*=UTF-8''" + encodedName) + .contentType(MediaType.parseMediaType(contentType)) + .contentLength(file.length()) + .body(new FileSystemResource(file)); + } + + /** 전체 파일 ZIP 다운로드 */ + @GetMapping("/download-all/{atchNo}") + public ResponseEntity downloadAll(@PathVariable String atchNo) throws IOException { + List> fileList = attachService.getFileList(atchNo); + if (fileList == null || fileList.isEmpty()) { + throw new BizException("다운로드할 파일이 없습니다.", HttpStatus.NOT_FOUND); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) { + for (Map info : fileList) { + String atchfileNo = (String) info.get("ATCHFILE_NO"); + String fileName = (String) info.get("ATCH_FILE_NM"); + try { + File physicalFile = attachService.getPhysicalFile(atchfileNo); + zos.putNextEntry(new ZipEntry(fileName != null ? fileName : atchfileNo)); + try (FileInputStream fis = new FileInputStream(physicalFile)) { + fis.transferTo(zos); + } + zos.closeEntry(); + } catch (Exception e) { + // 개별 파일 오류 시 건너뜀 + } + } + } + + String encodedName = URLEncoder.encode("첨부파일.zip", StandardCharsets.UTF_8) + .replace("+", "%20"); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename*=UTF-8''" + encodedName) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(baos.toByteArray()); + } + + /** 파일 삭제 */ + @DeleteMapping("/{atchfileNo}") + public ApiResponse delete(@PathVariable String atchfileNo) { + attachService.deleteFile(atchfileNo, false); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/company/gw/common/controller/AuthController.java b/backend/src/main/java/com/company/gw/common/controller/AuthController.java new file mode 100644 index 0000000..2d310d5 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/controller/AuthController.java @@ -0,0 +1,88 @@ +package com.company.gw.common.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.dto.LoginRequestDto; +import com.company.gw.common.dto.LoginResponseDto; +import com.company.gw.common.dto.RefreshRequestDto; +import com.company.gw.common.service.AuthService; +import com.company.gw.common.util.JwtUtil; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + private final JwtUtil jwtUtil; + + /** + * 로그인 + * POST /api/auth/login + */ + @PostMapping("/login") + public ResponseEntity> login( + @Valid @RequestBody LoginRequestDto req) { + LoginResponseDto result = authService.login(req); + return ResponseEntity.ok(ApiResponse.ok(result)); + } + + /** + * 로그아웃 + * POST /api/auth/logout + */ + @PostMapping("/logout") + public ResponseEntity> logout( + @AuthenticationPrincipal UserDetails userDetails) { + authService.logout(userDetails.getUsername()); + return ResponseEntity.ok(ApiResponse.ok(null)); + } + + /** + * Access Token 재발급 + * POST /api/auth/refresh + */ + @PostMapping("/refresh") + public ResponseEntity> refresh( + @Valid @RequestBody RefreshRequestDto req) { + LoginResponseDto.UserInfo userInfo = authService.refreshToken(req.getRefreshToken()); + String roleCode = (userInfo.getRoles() != null && !userInfo.getRoles().isEmpty()) + ? userInfo.getRoles().get(0) : "USER"; + String newAccessToken = jwtUtil.generateAccessToken(userInfo.getUsrId(), userInfo.getCorpNo(), roleCode); + return ResponseEntity.ok(ApiResponse.ok(newAccessToken)); + } + + /** + * 현재 로그인 사용자 정보 + * GET /api/auth/me + */ + @GetMapping("/me") + public ResponseEntity> me( + @AuthenticationPrincipal UserDetails userDetails) { + return ResponseEntity.ok(ApiResponse.ok(userDetails.getUsername())); + } + + /** + * 비밀번호 변경 + * POST /api/auth/change-pw + */ + @PostMapping("/change-pw") + public ResponseEntity> changePassword( + @AuthenticationPrincipal CurrentUser currentUser, + @RequestBody Map body) { + authService.changePassword( + currentUser.getUsrId(), + body.get("oldPw"), + body.get("newPw") + ); + return ResponseEntity.ok(ApiResponse.ok(null)); + } +} diff --git a/backend/src/main/java/com/company/gw/common/controller/CodeController.java b/backend/src/main/java/com/company/gw/common/controller/CodeController.java new file mode 100644 index 0000000..ffa596e --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/controller/CodeController.java @@ -0,0 +1,78 @@ +package com.company.gw.common.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.service.CodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.CacheManager; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@RestController +@RequestMapping("/api/code") +@RequiredArgsConstructor +public class CodeController { + + private final CodeService codeService; + private final CacheManager cacheManager; + + /** + * 공통코드 목록 조회 + * GET /api/code?commClCd=SX001&useYn=Y + */ + @GetMapping + public ApiResponse>> getCodeList( + @RequestParam String commClCd, + @RequestParam(defaultValue = "") String useYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(codeService.getCodeList(currentUser.getCorpNo(), commClCd, useYn)); + } + + /** + * 공통코드 전체 정보 조회 (PROP_CD 포함) + */ + @GetMapping("/full") + public ApiResponse>> getCodeListFull( + @RequestParam String commClCd, + @RequestParam(defaultValue = "") String useYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(codeService.getCodeListFull(currentUser.getCorpNo(), commClCd, useYn)); + } + + /** + * 직원 검색 (결재자 팝업 등) + * GET /api/code/users?searchText=홍&apprOnly=Y + */ + @GetMapping("/users") + public ApiResponse>> searchUser( + @RequestParam(defaultValue = "") String searchText, + @RequestParam(defaultValue = "N") String apprOnly, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(codeService.searchUser( + currentUser.getCorpNo(), searchText, "Y".equals(apprOnly))); + } + + /** + * 근무코드 목록 조회 + */ + @GetMapping("/workcd") + public ApiResponse>> getWorkCdList( + @RequestParam(defaultValue = "") String useYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(codeService.getWorkCdList(currentUser.getCorpNo(), useYn)); + } + + /** + * 캐시 초기화 (코드 변경 후 호출) + */ + @PostMapping("/cache/evict") + public ApiResponse evictCache() { + Objects.requireNonNull(cacheManager.getCache("code")).clear(); + Objects.requireNonNull(cacheManager.getCache("workCd")).clear(); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/company/gw/common/controller/ExcelController.java b/backend/src/main/java/com/company/gw/common/controller/ExcelController.java new file mode 100644 index 0000000..b4a1071 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/controller/ExcelController.java @@ -0,0 +1,116 @@ +package com.company.gw.common.controller; + +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.util.ExcelUtil; +import com.company.gw.tam.mapper.TamMapper; +import com.company.gw.wtime.mapper.WtimeMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Excel 내보내기 공통 컨트롤러 + */ +@RestController +@RequestMapping("/api/excel") +@RequiredArgsConstructor +public class ExcelController { + + private final WtimeMapper wtimeMapper; + private final TamMapper tamMapper; + + /** 근무시간 목록 엑셀 */ + @GetMapping("/wtime") + public ResponseEntity wtimeExcel( + @RequestParam Map params, + @AuthenticationPrincipal CurrentUser cu) throws IOException { + + params.put("corpNo", cu.getCorpNo()); + setDefault(params, "staYmd", "00000000"); + setDefault(params, "endYmd", "99999999"); + setDefault(params, "includeRetireYn", "N"); + setDefault(params, "includeWorkYn", "N"); + + List> data = wtimeMapper.getWtimeList(params); + + return ExcelUtil.toExcel("근무시간", + new String[]{"날짜", "이름", "팀", "직위", "근무코드", "출근", "퇴근", "총근무(분)", "지각(분)", "OT(분)", "결근"}, + new String[]{"WORK_PLAN_YYMMDD", "USR_NM", "TEAM_CD", "DUTY_CD", + "REAL_WORK_CD", "WORK_START_DT", "WORK_END_DT", + "TOT_WORK_MIN", "LATE_MIN", "OT_WORK_MIN", "ABTI_YN"}, + data); + } + + /** 월별 근무통계 엑셀 */ + @GetMapping("/wstat") + public ResponseEntity wstatExcel( + @RequestParam Map params, + @AuthenticationPrincipal CurrentUser cu) throws IOException { + + params.put("corpNo", cu.getCorpNo()); + setDefault(params, "staYmd", "00000000"); + setDefault(params, "endYmd", "99999999"); + setDefault(params, "includeRetireYn", "N"); + + List> data = wtimeMapper.getWstatList(params); + + return ExcelUtil.toExcel("근무통계", + new String[]{"이름", "팀", "직위", "연차", "근무일", "결근", "지각일", "조퇴일", "총근무(분)", "지각(분)", "OT(분)"}, + new String[]{"USR_NM", "TEAM_CD", "DUTY_CD", "YYVCT_CNT", + "WORK_DCNT", "ABTI_DCNT", "LATE_DCNT", "SKIPOFF_DCNT", + "TOT_WORK_MIN", "LATE_MIN", "OT_WORK_MIN"}, + data); + } + + /** 결재 신청 목록 엑셀 (TAM0020) */ + @GetMapping("/tam-apvreq") + public ResponseEntity tamApvreqExcel( + @RequestParam Map params, + @AuthenticationPrincipal CurrentUser cu) throws IOException { + + params.put("corpNo", cu.getCorpNo()); + params.put("usrId", cu.getUsrId()); + setDefault(params, "staYmd", "00000000"); + setDefault(params, "endYmd", "99999999"); + setDefault(params, "pageNo", 1); + setDefault(params, "pageSize", 10000); + + List> data = tamMapper.getApvreqList(params); + + return ExcelUtil.toExcel("결재신청목록", + new String[]{"문서ID", "상태", "종류", "상신일", "완료일"}, + new String[]{"APRVL_DOC_ID", "APRVL_STUS_CD", "APRVL_KIND_CD", "OFFER_DT", "CMPL_DT"}, + data); + } + + /** 근태 현황 엑셀 (TAM0040) */ + @GetMapping("/tam-status") + public ResponseEntity tamStatusExcel( + @RequestParam Map params, + @AuthenticationPrincipal CurrentUser cu) throws IOException { + + params.put("corpNo", cu.getCorpNo()); + setDefault(params, "staYmd", "00000000"); + setDefault(params, "endYmd", "99999999"); + setDefault(params, "includeRetireYn", "N"); + + List> data = tamMapper.getTamStatusList(params); + + return ExcelUtil.toExcel("근태현황", + new String[]{"이름", "팀", "직위", "연차", "결재건수"}, + new String[]{"USR_NM", "TEAM_CD", "DUTY_CD", "YYVCT_CNT", "APRVL_CNT"}, + data); + } + + private void setDefault(Map params, String key, Object defaultVal) { + if (!params.containsKey(key) || params.get(key) == null) { + params.put(key, defaultVal); + } + } +} diff --git a/backend/src/main/java/com/company/gw/common/dto/ApiResponse.java b/backend/src/main/java/com/company/gw/common/dto/ApiResponse.java new file mode 100644 index 0000000..2a74da3 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/ApiResponse.java @@ -0,0 +1,46 @@ +package com.company.gw.common.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + + private boolean success; + private T data; + private String message; + private PaginationInfo pagination; + + public static ApiResponse ok(T data) { + return ApiResponse.builder() + .success(true) + .data(data) + .build(); + } + + public static ApiResponse ok(T data, PaginationInfo pagination) { + return ApiResponse.builder() + .success(true) + .data(data) + .pagination(pagination) + .build(); + } + + public static ApiResponse fail(String message) { + return ApiResponse.builder() + .success(false) + .message(message) + .build(); + } + + @Getter + @Builder + public static class PaginationInfo { + private int page; + private int size; + private long total; + } +} diff --git a/backend/src/main/java/com/company/gw/common/dto/AttachDto.java b/backend/src/main/java/com/company/gw/common/dto/AttachDto.java new file mode 100644 index 0000000..5d22d63 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/AttachDto.java @@ -0,0 +1,16 @@ +package com.company.gw.common.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class AttachDto { + private String atchfileNo; + private String atchNo; + private String atchFilePathNm; + private String atchFileNm; + private String atchTypeNm; + private Long atchFileMg; + private String tblColNm; +} diff --git a/backend/src/main/java/com/company/gw/common/dto/ControllerRoleDto.java b/backend/src/main/java/com/company/gw/common/dto/ControllerRoleDto.java new file mode 100644 index 0000000..bb7b3db --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/ControllerRoleDto.java @@ -0,0 +1,12 @@ +package com.company.gw.common.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ControllerRoleDto { + private String controller; + private String role; + private String funcAuthCn; +} diff --git a/backend/src/main/java/com/company/gw/common/dto/CurrentUser.java b/backend/src/main/java/com/company/gw/common/dto/CurrentUser.java new file mode 100644 index 0000000..d6c2d66 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/CurrentUser.java @@ -0,0 +1,24 @@ +package com.company.gw.common.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.security.Principal; + +/** + * 기존 Grails의 SS_CORP_NO, SS_USER_ID, ROLE_CD를 대체하는 현재 로그인 사용자 정보. + * JWT 클레임에서 추출하여 컨트롤러/서비스에서 사용. + * Principal 구현 → auth.getName()이 usrId를 반환하도록 보장. + */ +@Getter +@Builder +public class CurrentUser implements Principal { + private final String usrId; + private final String corpNo; + private final String roleCode; + + @Override + public String getName() { + return usrId; + } +} diff --git a/backend/src/main/java/com/company/gw/common/dto/LoginRequestDto.java b/backend/src/main/java/com/company/gw/common/dto/LoginRequestDto.java new file mode 100644 index 0000000..8e9a7b2 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/LoginRequestDto.java @@ -0,0 +1,17 @@ +package com.company.gw.common.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class LoginRequestDto { + + @NotBlank(message = "로그인 아이디를 입력하세요.") + private String loginId; + + @NotBlank(message = "비밀번호를 입력하세요.") + private String loginPw; + + @NotBlank(message = "권한을 선택하세요.") + private String menuAuthCd; +} diff --git a/backend/src/main/java/com/company/gw/common/dto/LoginResponseDto.java b/backend/src/main/java/com/company/gw/common/dto/LoginResponseDto.java new file mode 100644 index 0000000..7a8cbcb --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/LoginResponseDto.java @@ -0,0 +1,27 @@ +package com.company.gw.common.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class LoginResponseDto { + private String accessToken; + private String refreshToken; + private UserInfo userInfo; + private List menuList; + + @Getter + @Builder + public static class UserInfo { + private String usrId; + private String usrNm; + private String loginId; + private String corpNo; + private String dutyCd; + private String roleCode; // 선택한 MENU_AUTH_CD + private List roles; // 보유 전체 role 목록 + } +} diff --git a/backend/src/main/java/com/company/gw/common/dto/MenuDto.java b/backend/src/main/java/com/company/gw/common/dto/MenuDto.java new file mode 100644 index 0000000..8b95898 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/MenuDto.java @@ -0,0 +1,18 @@ +package com.company.gw.common.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MenuDto { + private String menuNo; + private String upperMenuNo; + private String menuNm; + private String url; + private String rm; + private String menuProp; + private Integer menuOrdr; + private String menuUseYn; + private Integer lvl; +} diff --git a/backend/src/main/java/com/company/gw/common/dto/RefreshRequestDto.java b/backend/src/main/java/com/company/gw/common/dto/RefreshRequestDto.java new file mode 100644 index 0000000..0eb1e17 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/RefreshRequestDto.java @@ -0,0 +1,11 @@ +package com.company.gw.common.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class RefreshRequestDto { + + @NotBlank(message = "refreshToken이 필요합니다.") + private String refreshToken; +} diff --git a/backend/src/main/java/com/company/gw/common/dto/UserDto.java b/backend/src/main/java/com/company/gw/common/dto/UserDto.java new file mode 100644 index 0000000..06dca26 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/dto/UserDto.java @@ -0,0 +1,19 @@ +package com.company.gw.common.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserDto { + private String corpNo; + private String usrId; + private String usrNm; + private String loginId; + @JsonIgnore + private String pw; + private String dutyCd; + @JsonIgnore + private String retirementDate; +} diff --git a/backend/src/main/java/com/company/gw/common/exception/BizException.java b/backend/src/main/java/com/company/gw/common/exception/BizException.java new file mode 100644 index 0000000..9c3c483 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/exception/BizException.java @@ -0,0 +1,20 @@ +package com.company.gw.common.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class BizException extends RuntimeException { + + private final HttpStatus status; + + public BizException(String message) { + super(message); + this.status = HttpStatus.BAD_REQUEST; + } + + public BizException(String message, HttpStatus status) { + super(message); + this.status = status; + } +} diff --git a/backend/src/main/java/com/company/gw/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/company/gw/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b70a41f --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,42 @@ +package com.company.gw.common.exception; + +import com.company.gw.common.dto.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BizException.class) + public ResponseEntity> handleBizException(BizException e) { + log.warn("BizException: {}", e.getMessage()); + return ResponseEntity + .status(e.getStatus()) + .body(ApiResponse.fail(e.getMessage())); + } + + @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class}) + public ResponseEntity> handleValidationException(BindException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) + .findFirst() + .orElse("입력값이 올바르지 않습니다."); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.fail(message)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + log.error("Unhandled exception", e); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.fail("서버 오류가 발생했습니다.")); + } +} diff --git a/backend/src/main/java/com/company/gw/common/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/company/gw/common/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..83298c5 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/filter/JwtAuthenticationFilter.java @@ -0,0 +1,71 @@ +package com.company.gw.common.filter; + +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String token = resolveToken(request); + + if (StringUtils.hasText(token) && jwtUtil.isValid(token)) { + try { + String usrId = jwtUtil.getUserId(token); + String corpNo = jwtUtil.getCorpNo(token); + String roleCode = jwtUtil.getRoleCode(token); + + if (StringUtils.hasText(usrId)) { + CurrentUser currentUser = CurrentUser.builder() + .usrId(usrId) + .corpNo(corpNo) + .roleCode(roleCode) + .build(); + + var authorities = StringUtils.hasText(roleCode) + ? List.of(new SimpleGrantedAuthority("ROLE_" + roleCode)) + : List.of(); + + var authentication = new UsernamePasswordAuthenticationToken( + currentUser, null, authorities); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + log.debug("JWT 인증 실패 - 인증 없이 계속 진행: {}", e.getMessage()); + SecurityContextHolder.clearContext(); + } + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/backend/src/main/java/com/company/gw/common/mapper/AttachMapper.java b/backend/src/main/java/com/company/gw/common/mapper/AttachMapper.java new file mode 100644 index 0000000..b121d58 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/mapper/AttachMapper.java @@ -0,0 +1,24 @@ +package com.company.gw.common.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface AttachMapper { + + Map getFileInfo(String atchfileNo); + + List> getFileInfoListByAtchNo(String atchNo); + + void insertFileInfo(Map param); + + void deleteFileInfo(String atchfileNo); + + void updateTblColNmByAtchNo(Map param); + + void updateTblColNmByAtchfileNo(Map param); + + List> getNeedDeleteFileList(); +} diff --git a/backend/src/main/java/com/company/gw/common/mapper/CodeMapper.java b/backend/src/main/java/com/company/gw/common/mapper/CodeMapper.java new file mode 100644 index 0000000..cda183d --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/mapper/CodeMapper.java @@ -0,0 +1,18 @@ +package com.company.gw.common.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface CodeMapper { + + List> getCodeList(Map param); + + List> getCodeListFull(Map param); + + List> searchUser(Map param); + + List> getWorkCdList(Map param); +} diff --git a/backend/src/main/java/com/company/gw/common/mapper/CommonMapper.java b/backend/src/main/java/com/company/gw/common/mapper/CommonMapper.java new file mode 100644 index 0000000..be57acb --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/mapper/CommonMapper.java @@ -0,0 +1,12 @@ +package com.company.gw.common.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface CommonMapper { + + void increaseSequence(@Param("sequenceNm") String sequenceNm); + + Long getSequenceVal(@Param("sequenceNm") String sequenceNm); +} diff --git a/backend/src/main/java/com/company/gw/common/mapper/MenuMapper.java b/backend/src/main/java/com/company/gw/common/mapper/MenuMapper.java new file mode 100644 index 0000000..c07aef3 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/mapper/MenuMapper.java @@ -0,0 +1,17 @@ +package com.company.gw.common.mapper; + +import com.company.gw.common.dto.ControllerRoleDto; +import com.company.gw.common.dto.MenuDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface MenuMapper { + + List getMenuList(@Param("corpNo") String corpNo, + @Param("menuAuthCd") String menuAuthCd); + + List getControllerRoleList(@Param("corpNo") String corpNo); +} diff --git a/backend/src/main/java/com/company/gw/common/mapper/SecurityMapper.java b/backend/src/main/java/com/company/gw/common/mapper/SecurityMapper.java new file mode 100644 index 0000000..d253d6e --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/mapper/SecurityMapper.java @@ -0,0 +1,13 @@ +package com.company.gw.common.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface SecurityMapper { + + List getUserRoleList(@Param("corpNo") String corpNo, + @Param("usrId") String usrId); +} diff --git a/backend/src/main/java/com/company/gw/common/mapper/UserMapper.java b/backend/src/main/java/com/company/gw/common/mapper/UserMapper.java new file mode 100644 index 0000000..3c218b5 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/mapper/UserMapper.java @@ -0,0 +1,15 @@ +package com.company.gw.common.mapper; + +import com.company.gw.common.dto.UserDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserMapper { + + UserDto getUserInfo(@Param("usrId") String usrId); + + UserDto getUserInfoWithPwd(@Param("loginId") String loginId); + + int updatePassword(@Param("usrId") String usrId, @Param("newPw") String newPw); +} diff --git a/backend/src/main/java/com/company/gw/common/service/AttachService.java b/backend/src/main/java/com/company/gw/common/service/AttachService.java new file mode 100644 index 0000000..8ee675b --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/service/AttachService.java @@ -0,0 +1,226 @@ +package com.company.gw.common.service; + +import com.company.gw.common.dto.AttachDto; +import com.company.gw.common.exception.BizException; +import com.company.gw.common.mapper.AttachMapper; +import com.company.gw.common.util.SecurityUtil; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.FilenameUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class AttachService { + + private static final List NOT_ALLOWED_EXT = + Arrays.asList("php", "jsp", "asp", "gsp", "sh", "exe", "bat"); + + private static final List IMAGE_EXT = + Arrays.asList("jpg", "jpeg", "png", "gif", "tif", "tiff", "bmp", "webp"); + + private static final long MAX_FILE_SIZE = 30L * 1024 * 1024; // 30MB + + private final AttachMapper attachMapper; + + @Value("${file.upload-path}") + private String uploadPath; + + // ────────────────────────────────────────────── + // 업로드 + // ────────────────────────────────────────────── + + @Transactional + public AttachDto uploadFile(MultipartFile file, String atchNo, String division) { + validateFile(file); + + if (!org.springframework.util.StringUtils.hasText(atchNo)) { + atchNo = UUID.randomUUID().toString().replace("-", ""); + } + + String ext = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(); + String atchfileNo = UUID.randomUUID().toString().replace("-", ""); + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String relativePath = (division != null ? division : "common") + "/" + today + "/" + atchfileNo + "." + ext; + + File dest = new File(uploadPath, relativePath); + dest.getParentFile().mkdirs(); + try { + file.transferTo(dest); + } catch (IOException e) { + throw new BizException("파일 저장 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR); + } + + Map param = new HashMap<>(); + param.put("atchfileNo", atchfileNo); + param.put("atchNo", atchNo); + param.put("atchFilePathNm", relativePath); + param.put("atchFileNm", file.getOriginalFilename()); + param.put("atchTypeNm", file.getContentType()); + param.put("atchFileMg", file.getSize()); + param.put("tblColNm", null); + param.put("rgstrId", SecurityUtil.getUsrId()); + + attachMapper.insertFileInfo(param); + + return AttachDto.builder() + .atchfileNo(atchfileNo) + .atchNo(atchNo) + .atchFilePathNm(relativePath) + .atchFileNm(file.getOriginalFilename()) + .atchTypeNm(file.getContentType()) + .atchFileMg(file.getSize()) + .build(); + } + + @Transactional + public List uploadFiles(List files, String atchNo, String division) { + if (!org.springframework.util.StringUtils.hasText(atchNo)) { + atchNo = UUID.randomUUID().toString().replace("-", ""); + } + List result = new ArrayList<>(); + for (MultipartFile file : files) { + if (file != null && !file.isEmpty()) { + result.add(uploadFile(file, atchNo, division)); + } + } + return result; + } + + // 이미지 에디터용 업로드 (DB 저장 없이 파일만 저장) + public Map uploadEditorImage(MultipartFile file, String division) { + String ext = FilenameUtils.getExtension( + Objects.requireNonNull(file.getOriginalFilename())).toLowerCase(); + if (!IMAGE_EXT.contains(ext)) { + throw new BizException("이미지 파일만 업로드할 수 있습니다."); + } + if (file.getSize() > MAX_FILE_SIZE) { + throw new BizException("파일 크기가 초과되었습니다. (최대 30MB)"); + } + + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String fileId = UUID.randomUUID().toString().replace("-", ""); + String relativePath = (division != null ? division : "editor") + "/" + today + "/" + fileId + "." + ext; + + File dest = new File(uploadPath + "/images", relativePath); + dest.getParentFile().mkdirs(); + try { + file.transferTo(dest); + } catch (IOException e) { + throw new BizException("이미지 저장 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR); + } + + Map result = new HashMap<>(); + result.put("url", "/api/attach/image/" + relativePath.replace("/", "_") + "." + ext); + return result; + } + + // ────────────────────────────────────────────── + // 조회 + // ────────────────────────────────────────────── + + public Map getFileInfo(String atchfileNo) { + Map info = attachMapper.getFileInfo(atchfileNo); + if (info == null) { + throw new BizException("파일 정보가 존재하지 않습니다.", HttpStatus.NOT_FOUND); + } + return info; + } + + public List> getFileList(String atchNo) { + return attachMapper.getFileInfoListByAtchNo(atchNo); + } + + public File getPhysicalFile(String atchfileNo) { + Map info = getFileInfo(atchfileNo); + File file = new File(uploadPath, (String) info.get("ATCH_FILE_PATH_NM")); + if (!file.exists()) { + throw new BizException("파일이 존재하지 않습니다.", HttpStatus.NOT_FOUND); + } + return file; + } + + // ────────────────────────────────────────────── + // 삭제 + // ────────────────────────────────────────────── + + @Transactional + public void deleteFile(String atchfileNo, boolean forceDelete) { + Map info = getFileInfo(atchfileNo); + if (!forceDelete) { + String rgstrId = (String) info.get("RGSTR_ID"); + if (!SecurityUtil.getUsrId().equals(rgstrId)) { + throw new BizException("등록자만 삭제할 수 있습니다.", HttpStatus.FORBIDDEN); + } + } + attachMapper.deleteFileInfo(atchfileNo); + // 물리 파일 삭제 (DB 성공 후) + new File(uploadPath, (String) info.get("ATCH_FILE_PATH_NM")).delete(); + } + + @Transactional + public void deleteFilesByAtchNo(String atchNo, boolean forceDelete) { + List> list = attachMapper.getFileInfoListByAtchNo(atchNo); + for (Map info : list) { + deleteFile((String) info.get("ATCHFILE_NO"), forceDelete); + } + } + + // ────────────────────────────────────────────── + // TBL_COL_NM 확정 처리 (게시판/결재 저장 완료 후 호출) + // ────────────────────────────────────────────── + + @Transactional + public void confirmAtchNo(String atchNo, String tblColNm) { + if (!org.springframework.util.StringUtils.hasText(atchNo)) return; + Map param = new HashMap<>(); + param.put("atchNo", atchNo); + param.put("tblColNm", tblColNm); + attachMapper.updateTblColNmByAtchNo(param); + } + + // ────────────────────────────────────────────── + // 미확정 파일 정기 삭제 (6시간 이상 TBL_COL_NM 없는 파일) + // ────────────────────────────────────────────── + + @Scheduled(cron = "0 0 3 * * *") // 매일 새벽 3시 + @Transactional + public void cleanUpUnnecessaryFiles() { + List> list = attachMapper.getNeedDeleteFileList(); + for (Map item : list) { + try { + attachMapper.deleteFileInfo((String) item.get("ATCHFILE_NO")); + new File(uploadPath, (String) item.get("ATCH_FILE_PATH_NM")).delete(); + } catch (Exception ignored) { + } + } + } + + // ────────────────────────────────────────────── + // 검증 + // ────────────────────────────────────────────── + + private void validateFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new BizException("파일이 없습니다."); + } + String ext = FilenameUtils.getExtension( + Objects.requireNonNull(file.getOriginalFilename())).toLowerCase(); + if (NOT_ALLOWED_EXT.contains(ext)) { + throw new BizException("업로드할 수 없는 확장자입니다. (" + ext + ")"); + } + if (file.getSize() > MAX_FILE_SIZE) { + throw new BizException("파일 크기가 초과되었습니다. (최대 30MB)"); + } + } +} diff --git a/backend/src/main/java/com/company/gw/common/service/AuthService.java b/backend/src/main/java/com/company/gw/common/service/AuthService.java new file mode 100644 index 0000000..256d47d --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/service/AuthService.java @@ -0,0 +1,189 @@ +package com.company.gw.common.service; + +import com.company.gw.common.dto.*; +import com.company.gw.common.exception.BizException; +import com.company.gw.common.mapper.MenuMapper; +import com.company.gw.common.mapper.SecurityMapper; +import com.company.gw.common.mapper.UserMapper; +import com.company.gw.common.util.JwtUtil; +import com.company.gw.common.util.PasswordUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + private static final String ROLE_ADMIN = "BIDS_0001"; + private static final String ROLE_FEDEX = "FEDE"; + private static final String ROLE_USER = "USER"; + private static final String REFRESH_TOKEN_PREFIX = "refresh:"; + + private final UserMapper userMapper; + private final SecurityMapper securityMapper; + private final MenuMapper menuMapper; + private final JwtUtil jwtUtil; + private final RedisTemplate redisTemplate; + + /** + * 로그인 + * 기존 IndexController.do_login() 로직과 동일 + */ + public LoginResponseDto login(LoginRequestDto req) { + + // 1. 사용자 조회 (PW 포함) + UserDto user = userMapper.getUserInfoWithPwd(req.getLoginId()); + if (user == null) { + throw new BizException("로그인 아이디 또는 비밀번호를 확인하세요."); + } + + // 2. 비밀번호 검증 (SHA-256 + Base64) + if (!PasswordUtil.matches(req.getLoginPw(), user.getPw())) { + throw new BizException("로그인 아이디 또는 비밀번호를 확인하세요."); + } + + // 3. 퇴직자 체크 + if (StringUtils.hasText(user.getRetirementDate())) { + throw new BizException("로그인 아이디 또는 비밀번호를 확인하세요."); + } + + // 4. 보유 롤 조회 + List roles = securityMapper.getUserRoleList(user.getCorpNo(), user.getUsrId()); + + // 5. 선택한 MENU_AUTH_CD 보유 여부 검증 + // 구 시스템(SecurityService.groovy) 로직과 동일: + // - FEDE 사용자이거나, USER 이외의 권한을 선택한 경우에만 DB 체크 + // - USER 권한은 FEDE 사용자가 아니면 누구나 사용 가능 (기본값) + boolean isFedexUser = roles.contains(ROLE_FEDEX); + String menuAuthCd = req.getMenuAuthCd(); + if (isFedexUser || !ROLE_USER.equals(menuAuthCd)) { + if (!roles.contains(menuAuthCd)) { + throw new BizException("해당 메뉴 권한이 없습니다."); + } + } + + // 6. JWT 발급 (usrId + corpNo + roleCode 포함) + String accessToken = jwtUtil.generateAccessToken(user.getUsrId(), user.getCorpNo(), menuAuthCd); + String refreshToken = jwtUtil.generateRefreshToken(user.getUsrId()); + + // 7. Redis에 Refresh Token 저장 (7일) — Redis 미사용 환경에서는 건너뜀 + try { + redisTemplate.opsForValue().set( + REFRESH_TOKEN_PREFIX + user.getUsrId(), + refreshToken, + 7, TimeUnit.DAYS + ); + } catch (Exception e) { + log.warn("Redis 미사용 환경: Refresh Token 저장 건너뜀 ({})", e.getMessage()); + } + + // 8. 메뉴 조회 - 선택한 역할 코드로 SX_CO0090 기반 필터링 + List menuList = menuMapper.getMenuList(user.getCorpNo(), menuAuthCd); + + return LoginResponseDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .userInfo(LoginResponseDto.UserInfo.builder() + .usrId(user.getUsrId()) + .usrNm(user.getUsrNm()) + .loginId(user.getLoginId()) + .corpNo(user.getCorpNo()) + .dutyCd(user.getDutyCd()) + .roleCode(menuAuthCd) + .roles(roles) + .build()) + .menuList(menuList) + .build(); + } + + /** + * Access Token 재발급 (Refresh Token 검증) + */ + public LoginResponseDto.UserInfo refreshToken(String refreshToken) { + if (!jwtUtil.isValid(refreshToken)) { + throw new BizException("유효하지 않은 토큰입니다.", HttpStatus.UNAUTHORIZED); + } + + String usrId = jwtUtil.getUserId(refreshToken); + try { + String stored = redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + usrId); + if (stored != null && !refreshToken.equals(stored)) { + throw new BizException("만료된 토큰입니다. 다시 로그인하세요.", HttpStatus.UNAUTHORIZED); + } + // stored == null: Redis 미사용 환경 → JWT 유효성만으로 통과 + } catch (BizException e) { + throw e; + } catch (Exception e) { + log.warn("Redis 미사용 환경: Refresh Token 검증 건너뜀 ({})", e.getMessage()); + } + + // 사용자 정보 재조회 + UserDto user = userMapper.getUserInfo(usrId); + if (user == null) { + throw new BizException("사용자 정보를 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED); + } + + List roles = securityMapper.getUserRoleList(user.getCorpNo(), usrId); + + return LoginResponseDto.UserInfo.builder() + .usrId(user.getUsrId()) + .usrNm(user.getUsrNm()) + .loginId(user.getLoginId()) + .corpNo(user.getCorpNo()) + .dutyCd(user.getDutyCd()) + .roles(roles) + .build(); + } + + /** + * 로그아웃 - Redis에서 Refresh Token 삭제 + */ + public void logout(String usrId) { + try { + redisTemplate.delete(REFRESH_TOKEN_PREFIX + usrId); + } catch (Exception e) { + log.warn("Redis 미사용 환경: 로그아웃 토큰 삭제 건너뜀 ({})", e.getMessage()); + } + log.debug("로그아웃 처리 완료: {}", usrId); + } + + /** + * 비밀번호 변경 + */ + @org.springframework.transaction.annotation.Transactional + public void changePassword(String usrId, String oldPw, String newPw) { + UserDto user = userMapper.getUserInfo(usrId); + if (user == null) { + throw new BizException("사용자 정보를 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED); + } + // 현재 비밀번호 확인 (loginId로 재조회하여 PW 포함) + UserDto userWithPw = userMapper.getUserInfoWithPwd(user.getLoginId()); + if (userWithPw == null || !PasswordUtil.matches(oldPw, userWithPw.getPw())) { + throw new BizException("현재 비밀번호가 올바르지 않습니다."); + } + if (!StringUtils.hasText(newPw) || newPw.length() < 4) { + throw new BizException("새 비밀번호는 4자 이상이어야 합니다."); + } + userMapper.updatePassword(usrId, PasswordUtil.encode(newPw)); + try { + redisTemplate.delete(REFRESH_TOKEN_PREFIX + usrId); + } catch (Exception e) { + log.warn("Redis 미사용 환경: 비밀번호 변경 후 토큰 삭제 건너뜀 ({})", e.getMessage()); + } + } + + /** + * 관리자 여부 + */ + public boolean isAdmin(List roles) { + return roles.contains(ROLE_ADMIN); + } +} diff --git a/backend/src/main/java/com/company/gw/common/service/CodeService.java b/backend/src/main/java/com/company/gw/common/service/CodeService.java new file mode 100644 index 0000000..32a44f7 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/service/CodeService.java @@ -0,0 +1,72 @@ +package com.company.gw.common.service; + +import com.company.gw.common.mapper.CodeMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 공통코드 조회 서비스 + * - @Cacheable("code") 로 Redis 캐시 적용 (기본 TTL은 RedisConfig에서 설정) + * - 공통코드 변경 시 /api/code/cache/evict 호출로 캐시 초기화 + */ +@Service +@RequiredArgsConstructor +public class CodeService { + + private final CodeMapper codeMapper; + + // ────────────────────────────────────────────── + // 공통코드 목록 (드롭다운용) + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + @Cacheable(value = "code", key = "#corpNo + ':' + #commClCd + ':' + #useYn") + public List> getCodeList(String corpNo, String commClCd, String useYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("commClCd", commClCd); + p.put("useYn", useYn); + return codeMapper.getCodeList(p); + } + + @Transactional(readOnly = true) + public List> getCodeListFull(String corpNo, String commClCd, String useYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("commClCd", commClCd); + p.put("useYn", useYn); + return codeMapper.getCodeListFull(p); + } + + // ────────────────────────────────────────────── + // 직원 검색 (결재자 선택 팝업 등) + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> searchUser(String corpNo, String searchText, boolean apprOnly) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("searchText", searchText); + p.put("apprOnly", apprOnly ? "Y" : "N"); + return codeMapper.searchUser(p); + } + + // ────────────────────────────────────────────── + // 근무코드 목록 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + @Cacheable(value = "workCd", key = "#corpNo + ':' + #useYn") + public List> getWorkCdList(String corpNo, String useYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("useYn", useYn); + return codeMapper.getWorkCdList(p); + } +} diff --git a/backend/src/main/java/com/company/gw/common/service/CustomUserDetailsService.java b/backend/src/main/java/com/company/gw/common/service/CustomUserDetailsService.java new file mode 100644 index 0000000..f2d8a75 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/service/CustomUserDetailsService.java @@ -0,0 +1,41 @@ +package com.company.gw.common.service; + +import com.company.gw.common.dto.UserDto; +import com.company.gw.common.mapper.SecurityMapper; +import com.company.gw.common.mapper.UserMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserMapper userMapper; + private final SecurityMapper securityMapper; + + @Override + public UserDetails loadUserByUsername(String usrId) throws UsernameNotFoundException { + UserDto user = userMapper.getUserInfo(usrId); + if (user == null) { + throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + usrId); + } + + List roles = securityMapper.getUserRoleList(user.getCorpNo(), usrId); + List authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .toList(); + + return User.builder() + .username(usrId) + .password("") // JWT 방식이므로 password 불필요 + .authorities(authorities) + .build(); + } +} diff --git a/backend/src/main/java/com/company/gw/common/service/SequenceService.java b/backend/src/main/java/com/company/gw/common/service/SequenceService.java new file mode 100644 index 0000000..752e2f0 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/service/SequenceService.java @@ -0,0 +1,24 @@ +package com.company.gw.common.service; + +import com.company.gw.common.mapper.CommonMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 기존 CommonService.getNextSeqString() 대체. + * SX_CO0060 테이블의 MERGE 기반 시퀀스. + */ +@Service +@RequiredArgsConstructor +public class SequenceService { + + private final CommonMapper commonMapper; + + @Transactional + public String getNextSeqString(String sequenceNm, int padLength) { + commonMapper.increaseSequence(sequenceNm); + Long val = commonMapper.getSequenceVal(sequenceNm); + return String.format("%0" + padLength + "d", val); + } +} diff --git a/backend/src/main/java/com/company/gw/common/util/ExcelUtil.java b/backend/src/main/java/com/company/gw/common/util/ExcelUtil.java new file mode 100644 index 0000000..2fb0d9e --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/util/ExcelUtil.java @@ -0,0 +1,86 @@ +package com.company.gw.common.util; + +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * Excel 내보내기 유틸리티 + */ +public class ExcelUtil { + + private ExcelUtil() {} + + /** + * Map 리스트를 Excel로 변환하여 ResponseEntity 반환 + * @param fileName 다운로드 파일명 (확장자 없이) + * @param headers 컬럼 헤더 배열 + * @param keys Map에서 읽을 키 배열 (headers와 1:1 대응) + * @param data 데이터 리스트 + */ + public static ResponseEntity toExcel(String fileName, + String[] headers, + String[] keys, + List> data) throws IOException { + try (Workbook wb = new XSSFWorkbook()) { + Sheet sheet = wb.createSheet("Sheet1"); + + // 헤더 스타일 + CellStyle headerStyle = wb.createCellStyle(); + Font headerFont = wb.createFont(); + headerFont.setBold(true); + headerStyle.setFont(headerFont); + headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + headerStyle.setBorderBottom(BorderStyle.THIN); + + // 헤더 행 + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerStyle); + sheet.setColumnWidth(i, 4000); + } + + // 데이터 행 + int rowIdx = 1; + for (Map row : data) { + Row dataRow = sheet.createRow(rowIdx++); + for (int i = 0; i < keys.length; i++) { + Cell cell = dataRow.createCell(i); + Object val = row.get(keys[i]); + if (val == null) { + cell.setCellValue(""); + } else if (val instanceof Number) { + cell.setCellValue(((Number) val).doubleValue()); + } else { + cell.setCellValue(val.toString()); + } + } + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + wb.write(baos); + + String encodedName = URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8) + .replace("+", "%20"); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename*=UTF-8''" + encodedName) + .contentType(MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) + .body(baos.toByteArray()); + } + } +} diff --git a/backend/src/main/java/com/company/gw/common/util/JwtUtil.java b/backend/src/main/java/com/company/gw/common/util/JwtUtil.java new file mode 100644 index 0000000..ca6622b --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/util/JwtUtil.java @@ -0,0 +1,82 @@ +package com.company.gw.common.util; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Slf4j +@Component +public class JwtUtil { + + private static final String CLAIM_CORP_NO = "corpNo"; + private static final String CLAIM_ROLE_CODE = "roleCode"; + + private final SecretKey secretKey; + private final long accessTokenExpiry; + private final long refreshTokenExpiry; + + public JwtUtil( + @Value("${jwt.secret}") String secret, + @Value("${jwt.access-token-expiry}") long accessTokenExpiry, + @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessTokenExpiry = accessTokenExpiry; + this.refreshTokenExpiry = refreshTokenExpiry; + } + + public String generateAccessToken(String usrId, String corpNo, String roleCode) { + return Jwts.builder() + .subject(usrId) + .claim(CLAIM_CORP_NO, corpNo) + .claim(CLAIM_ROLE_CODE, roleCode) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + accessTokenExpiry)) + .signWith(secretKey) + .compact(); + } + + public String generateRefreshToken(String usrId) { + return Jwts.builder() + .subject(usrId) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + refreshTokenExpiry)) + .signWith(secretKey) + .compact(); + } + + public String getUserId(String token) { + return getClaims(token).getSubject(); + } + + public String getCorpNo(String token) { + return (String) getClaims(token).get(CLAIM_CORP_NO); + } + + public String getRoleCode(String token) { + return (String) getClaims(token).get(CLAIM_ROLE_CODE); + } + + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.debug("Invalid JWT token: {}", e.getMessage()); + return false; + } + } + + private Claims getClaims(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} diff --git a/backend/src/main/java/com/company/gw/common/util/PasswordUtil.java b/backend/src/main/java/com/company/gw/common/util/PasswordUtil.java new file mode 100644 index 0000000..f5db573 --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/util/PasswordUtil.java @@ -0,0 +1,30 @@ +package com.company.gw.common.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * 기존 Grails EncryptUtil.get_enc_password() 와 동일한 방식 + * SHA-256 → Base64 인코딩 + * 기존 Oracle DB의 PW 컬럼 값과 호환 필수 + */ +public class PasswordUtil { + + private PasswordUtil() {} + + public static String encode(String rawPassword) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(rawPassword.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 알고리즘을 찾을 수 없습니다.", e); + } + } + + public static boolean matches(String rawPassword, String encodedPassword) { + return encode(rawPassword).equals(encodedPassword); + } +} diff --git a/backend/src/main/java/com/company/gw/common/util/SecurityUtil.java b/backend/src/main/java/com/company/gw/common/util/SecurityUtil.java new file mode 100644 index 0000000..5f30cfc --- /dev/null +++ b/backend/src/main/java/com/company/gw/common/util/SecurityUtil.java @@ -0,0 +1,52 @@ +package com.company.gw.common.util; + +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.exception.BizException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * JWT에서 현재 사용자 정보를 추출하는 유틸. + * 기존 CommonUtil.getLoginCorpNo(), getLoginUserID() 역할. + */ +public class SecurityUtil { + + private SecurityUtil() {} + + public static CurrentUser getCurrentUser(JwtUtil jwtUtil) { + String token = resolveToken(); + if (!StringUtils.hasText(token) || !jwtUtil.isValid(token)) { + throw new BizException("인증 정보가 없습니다.", HttpStatus.UNAUTHORIZED); + } + return CurrentUser.builder() + .usrId(jwtUtil.getUserId(token)) + .corpNo(jwtUtil.getCorpNo(token)) + .roleCode(jwtUtil.getRoleCode(token)) + .build(); + } + + public static String getUsrId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new BizException("인증 정보가 없습니다.", HttpStatus.UNAUTHORIZED); + } + return auth.getName(); + } + + private static String resolveToken() { + ServletRequestAttributes attrs = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attrs == null) return null; + HttpServletRequest request = attrs.getRequest(); + String bearer = request.getHeader("Authorization"); + if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) { + return bearer.substring(7); + } + return null; + } +} diff --git a/backend/src/main/java/com/company/gw/envset/controller/CodeManageController.java b/backend/src/main/java/com/company/gw/envset/controller/CodeManageController.java new file mode 100644 index 0000000..1331027 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/controller/CodeManageController.java @@ -0,0 +1,62 @@ +package com.company.gw.envset.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.util.JwtUtil; +import com.company.gw.common.util.SecurityUtil; +import com.company.gw.envset.dto.CodeDto; +import com.company.gw.envset.dto.CodeIndexDto; +import com.company.gw.envset.service.CodeManageService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 환경설정 > 공통코드 관리 (Envset0020/0030) + * GET /api/envset/codes 코드인덱스 목록 + * POST /api/envset/codes 코드인덱스 배치 저장 + * GET /api/envset/codes/{commClCd} 코드 목록 + * POST /api/envset/codes/{commClCd} 코드 배치 저장 + */ +@RestController +@RequestMapping("/api/envset/codes") +@RequiredArgsConstructor +public class CodeManageController { + + private final CodeManageService codeManageService; + private final JwtUtil jwtUtil; + + @GetMapping + public ResponseEntity>> getCdidxList( + @RequestParam(required = false) String searchText) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ResponseEntity.ok(ApiResponse.ok(codeManageService.getCdidxList(cu, searchText))); + } + + @PostMapping + public ResponseEntity> saveCdidxList(@RequestBody List list) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + codeManageService.saveCdidxList(cu, list); + return ResponseEntity.ok(ApiResponse.ok(null)); + } + + @GetMapping("/{commClCd}") + public ResponseEntity>> getCodeList( + @PathVariable String commClCd, + @RequestParam(required = false) String searchText) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ResponseEntity.ok(ApiResponse.ok( + codeManageService.getCodeList(cu, commClCd, searchText))); + } + + @PostMapping("/{commClCd}") + public ResponseEntity> saveCodeList( + @PathVariable String commClCd, + @RequestBody List list) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + codeManageService.saveCodeList(cu, commClCd, list); + return ResponseEntity.ok(ApiResponse.ok(null)); + } +} diff --git a/backend/src/main/java/com/company/gw/envset/controller/MenuManageController.java b/backend/src/main/java/com/company/gw/envset/controller/MenuManageController.java new file mode 100644 index 0000000..53a0d68 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/controller/MenuManageController.java @@ -0,0 +1,89 @@ +package com.company.gw.envset.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.util.JwtUtil; +import com.company.gw.common.util.SecurityUtil; +import com.company.gw.envset.dto.AuthMenuDto; +import com.company.gw.envset.dto.MenuManageDto; +import com.company.gw.envset.dto.UserAuthDto; +import com.company.gw.envset.service.MenuManageService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 환경설정 > 메뉴관리 / 사용자권한 (Envset0050) + * + * GET /api/envset/menus 메뉴 목록 + * POST /api/envset/menus 메뉴 배치 저장 + * GET /api/envset/menus/auth?menuAuthCd= 권한별 메뉴 목록 + * POST /api/envset/menus/auth?menuAuthCd= 권한-메뉴 배치 저장 + * GET /api/envset/menus/user-auth 사용자-권한 목록 + * POST /api/envset/menus/user-auth 사용자-권한 배치 저장 + * GET /api/envset/menus/search-user 사용자 검색 (autocomplete) + */ +@RestController +@RequestMapping("/api/envset/menus") +@RequiredArgsConstructor +public class MenuManageController { + + private final MenuManageService menuManageService; + private final JwtUtil jwtUtil; + + @GetMapping + public ResponseEntity>> getMenuList() { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ResponseEntity.ok(ApiResponse.ok(menuManageService.getMenuList(cu))); + } + + @PostMapping + public ResponseEntity> saveMenuList(@RequestBody List list) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + menuManageService.saveMenuList(cu, list); + return ResponseEntity.ok(ApiResponse.ok(null)); + } + + @GetMapping("/auth") + public ResponseEntity>> getAuthMenuList( + @RequestParam String menuAuthCd) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ResponseEntity.ok(ApiResponse.ok( + menuManageService.getAuthMenuList(cu, menuAuthCd))); + } + + @PostMapping("/auth") + public ResponseEntity> saveAuthMenuList( + @RequestParam String menuAuthCd, + @RequestBody List list) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + menuManageService.saveAuthMenuList(cu, menuAuthCd, list); + return ResponseEntity.ok(ApiResponse.ok(null)); + } + + @GetMapping("/user-auth") + public ResponseEntity>> getUserMauthList( + @RequestParam(required = false) String menuAuthCd, + @RequestParam(required = false) String usrNm) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ResponseEntity.ok(ApiResponse.ok( + menuManageService.getUserMauthList(cu, menuAuthCd, usrNm))); + } + + @PostMapping("/user-auth") + public ResponseEntity> saveUserMauthList(@RequestBody List list) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + menuManageService.saveUserMauthList(cu, list); + return ResponseEntity.ok(ApiResponse.ok(null)); + } + + @GetMapping("/search-user") + public ResponseEntity>>> searchUser( + @RequestParam(required = false) String keyword) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ResponseEntity.ok(ApiResponse.ok(menuManageService.searchUser(cu, keyword))); + } +} diff --git a/backend/src/main/java/com/company/gw/envset/controller/UserManageController.java b/backend/src/main/java/com/company/gw/envset/controller/UserManageController.java new file mode 100644 index 0000000..68d5730 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/controller/UserManageController.java @@ -0,0 +1,95 @@ +package com.company.gw.envset.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.util.JwtUtil; +import com.company.gw.common.util.SecurityUtil; +import com.company.gw.envset.dto.UserDetailDto; +import com.company.gw.envset.dto.UserSaveDto; +import com.company.gw.envset.service.UserManageService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * 환경설정 > 직원정보 (Envset0010) + * GET /api/envset/users 직원 목록 (페이징) + * GET /api/envset/users/{usrId} 직원 상세 + * POST /api/envset/users 직원 등록 + * PUT /api/envset/users/{usrId} 직원 수정 + * DELETE /api/envset/users/{usrId} 직원 삭제 + */ +@RestController +@RequestMapping("/api/envset/users") +@RequiredArgsConstructor +public class UserManageController { + + private final UserManageService userManageService; + private final JwtUtil jwtUtil; + + @GetMapping + public ResponseEntity>> getUserList( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "100") int size, + @RequestParam(required = false) String usrNm, + @RequestParam(required = false) String teamCd, + @RequestParam(required = false) String dutyCd, + @RequestParam(required = false) String apprYn, + @RequestParam(defaultValue = "N") String includeRetireYn, + @RequestParam(defaultValue = "USR_ID") String userOrderBy) { + + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + + Map params = new HashMap<>(); + params.put("page", page); + params.put("size", size); + params.put("usrNm", usrNm); + params.put("teamCd", teamCd); + params.put("dutyCd", dutyCd); + params.put("apprYn", apprYn); + params.put("includeRetireYn", includeRetireYn); + params.put("userOrderBy", userOrderBy); + + Map result = userManageService.getUserList(cu, params); + long total = (long) result.get("total"); + + return ResponseEntity.ok(ApiResponse.ok( + result, + ApiResponse.PaginationInfo.builder().page(page).size(size).total(total).build() + )); + } + + @GetMapping("/{usrId}") + public ResponseEntity> getUserDetail(@PathVariable String usrId) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ResponseEntity.ok(ApiResponse.ok(userManageService.getUserDetail(cu, usrId))); + } + + @PostMapping + public ResponseEntity> insertUser(@Valid @RequestBody UserSaveDto dto) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + userManageService.insertUser(cu, dto); + return ResponseEntity.ok(ApiResponse.ok(null)); + } + + @PutMapping("/{usrId}") + public ResponseEntity> updateUser( + @PathVariable String usrId, + @Valid @RequestBody UserSaveDto dto) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + dto.setUsrId(usrId); + userManageService.updateUser(cu, dto); + return ResponseEntity.ok(ApiResponse.ok(null)); + } + + @DeleteMapping("/{usrId}") + public ResponseEntity> deleteUser(@PathVariable String usrId) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + userManageService.deleteUser(cu, usrId); + return ResponseEntity.ok(ApiResponse.ok(null)); + } +} diff --git a/backend/src/main/java/com/company/gw/envset/controller/WorkCdController.java b/backend/src/main/java/com/company/gw/envset/controller/WorkCdController.java new file mode 100644 index 0000000..005956f --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/controller/WorkCdController.java @@ -0,0 +1,58 @@ +package com.company.gw.envset.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.util.JwtUtil; +import com.company.gw.common.util.SecurityUtil; +import com.company.gw.envset.service.WorkCdService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 환경설정 > 근무코드 관리 (Envset0040, SX_CO0070) + * GET /api/envset/workcd 목록 + * POST /api/envset/workcd 등록 + * PUT /api/envset/workcd/{workCd} 수정 + * DELETE /api/envset/workcd/{workCd} 삭제 + */ +@RestController +@RequestMapping("/api/envset/workcd") +@RequiredArgsConstructor +public class WorkCdController { + + private final WorkCdService workCdService; + private final JwtUtil jwtUtil; + + @GetMapping + public ApiResponse>> getWorkCdList( + @RequestParam(defaultValue = "") String searchText) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + return ApiResponse.ok(workCdService.getWorkCdList(cu.getCorpNo(), searchText)); + } + + @PostMapping + public ApiResponse insertWorkCd(@RequestBody Map body) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + workCdService.insertWorkCd(cu.getCorpNo(), cu.getUsrId(), body); + return ApiResponse.ok(null); + } + + @PutMapping("/{workCd}") + public ApiResponse updateWorkCd( + @PathVariable String workCd, + @RequestBody Map body) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + workCdService.updateWorkCd(cu.getCorpNo(), cu.getUsrId(), workCd, body); + return ApiResponse.ok(null); + } + + @DeleteMapping("/{workCd}") + public ApiResponse deleteWorkCd(@PathVariable String workCd) { + CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil); + workCdService.deleteWorkCd(cu.getCorpNo(), workCd); + return ApiResponse.ok(null); + } +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/AuthMenuDto.java b/backend/src/main/java/com/company/gw/envset/dto/AuthMenuDto.java new file mode 100644 index 0000000..7d5ff87 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/AuthMenuDto.java @@ -0,0 +1,23 @@ +package com.company.gw.envset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class AuthMenuDto { + private String corpNo; + private String menuNo; + private String upperMenuNo; + private String menuNm; + private String url; + private String menuProp; + private Integer menuOrdr; + private String menuUseYn; + private Integer lvl; + private String menuAuthYn; // Y/N + private String menuAuthCd; + private String funcAuthCn; + private String rgstrId; + private String modId; + private String rowStatus; // U만 존재 (Y이면 merge, N이면 delete) +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/CodeDto.java b/backend/src/main/java/com/company/gw/envset/dto/CodeDto.java new file mode 100644 index 0000000..36e100d --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/CodeDto.java @@ -0,0 +1,18 @@ +package com.company.gw.envset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class CodeDto { + private String corpNo; + private String commClCd; + private String commCd; + private String commCdNm; + private String commCdUseYn; + private String commCdDscrpt; + private Integer commCdDsplyOrdr; + private String rgstrId; + private String modId; + private String rowStatus; // I/U/D +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/CodeIndexDto.java b/backend/src/main/java/com/company/gw/envset/dto/CodeIndexDto.java new file mode 100644 index 0000000..a96a48d --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/CodeIndexDto.java @@ -0,0 +1,18 @@ +package com.company.gw.envset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class CodeIndexDto { + private String corpNo; + private String commClCd; + private String commClCdNm; + private String commClCdUseYn; + private String commClCdDscrpt; + private Integer dsplyOrdr; + private String rgstrId; + private String modId; + // 배치 저장용 상태 플래그 + private String rowStatus; // I/U/D +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/MenuManageDto.java b/backend/src/main/java/com/company/gw/envset/dto/MenuManageDto.java new file mode 100644 index 0000000..af8d341 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/MenuManageDto.java @@ -0,0 +1,22 @@ +package com.company.gw.envset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class MenuManageDto { + private String corpNo; + private String menuNo; + private String upperMenuNo; + private String menuNm; + private String url; + private String rm; + private String menuProp; + private Integer menuOrdr; + private String menuUseYn; + private String controller; + private Integer menuLvl; + private String rgstrId; + private String modId; + private String rowStatus; // I/U/D +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/UserAuthDto.java b/backend/src/main/java/com/company/gw/envset/dto/UserAuthDto.java new file mode 100644 index 0000000..77fea83 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/UserAuthDto.java @@ -0,0 +1,17 @@ +package com.company.gw.envset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class UserAuthDto { + private String corpNo; + private String usrId; + private String usrNm; + private String menuAuthCd; + private String teamCd; + private String dutyCd; + private String retirementDate; + private String rgstrId; + private String rowStatus; // I/D +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/UserDetailDto.java b/backend/src/main/java/com/company/gw/envset/dto/UserDetailDto.java new file mode 100644 index 0000000..b8b9ca6 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/UserDetailDto.java @@ -0,0 +1,31 @@ +package com.company.gw.envset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class UserDetailDto { + private String corpNo; + private String usrId; + private String usrSortOrdr; + private String usrNm; + private String loginId; + private String dutyCd; + private String brthDyDate; + private String usrTelno; + private String mtelNo; + private String email; + private String baseAdrs; + private String gusoAdrs; + private String gusoTelno; + private String joinCpDate; + private String teamCd; + private String dismlDate; + private String retirementDate; + private String spkltArtcCn; + private String photoAtchfileNo; + private String spmtEmail; + private String spmtMtelNo; + private String apprYn; + private String finalSchspNm; +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/UserListDto.java b/backend/src/main/java/com/company/gw/envset/dto/UserListDto.java new file mode 100644 index 0000000..60f2d45 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/UserListDto.java @@ -0,0 +1,15 @@ +package com.company.gw.envset.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class UserListDto { + private String usrId; + private String usrNm; + private String teamCd; + private String dutyCd; + private String usrTelno; + private String mtelNo; + private String retirementDate; +} diff --git a/backend/src/main/java/com/company/gw/envset/dto/UserSaveDto.java b/backend/src/main/java/com/company/gw/envset/dto/UserSaveDto.java new file mode 100644 index 0000000..a108f68 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/dto/UserSaveDto.java @@ -0,0 +1,39 @@ +package com.company.gw.envset.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class UserSaveDto { + // 공통 (insert/update 구분용) + private String corpNo; + private String usrId; + private String rgstrId; + private String modId; + + @NotBlank(message = "직원명은 필수입니다.") + private String usrNm; + + private String loginId; + private String pw; // 평문 → 서비스에서 암호화 + private String usrSortOrdr; + private String dutyCd; + private String brthDyDate; + private String usrTelno; + private String mtelNo; + private String email; + private String baseAdrs; + private String gusoAdrs; + private String gusoTelno; + private String joinCpDate; + private String teamCd; + private String dismlDate; + private String retirementDate; + private String spkltArtcCn; + private String spmtEmail; + private String spmtMtelNo; + private String rrno; + private String apprYn; + private String finalSchspNm; +} diff --git a/backend/src/main/java/com/company/gw/envset/mapper/CodeManageMapper.java b/backend/src/main/java/com/company/gw/envset/mapper/CodeManageMapper.java new file mode 100644 index 0000000..fd26ece --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/mapper/CodeManageMapper.java @@ -0,0 +1,25 @@ +package com.company.gw.envset.mapper; + +import com.company.gw.envset.dto.CodeDto; +import com.company.gw.envset.dto.CodeIndexDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface CodeManageMapper { + List getCdidxList(Map params); + CodeIndexDto getCdidxInfo(Map params); + void insertCdidx(CodeIndexDto dto); + void updateCdidx(CodeIndexDto dto); + void deleteCdidx(@Param("corpNo") String corpNo, @Param("commClCd") String commClCd); + + List getCodeList(Map params); + void insertCode(CodeDto dto); + void updateCode(CodeDto dto); + void deleteCode(@Param("corpNo") String corpNo, + @Param("commClCd") String commClCd, + @Param("commCd") String commCd); +} diff --git a/backend/src/main/java/com/company/gw/envset/mapper/MenuManageMapper.java b/backend/src/main/java/com/company/gw/envset/mapper/MenuManageMapper.java new file mode 100644 index 0000000..3b59f5b --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/mapper/MenuManageMapper.java @@ -0,0 +1,33 @@ +package com.company.gw.envset.mapper; + +import com.company.gw.envset.dto.AuthMenuDto; +import com.company.gw.envset.dto.MenuManageDto; +import com.company.gw.envset.dto.UserAuthDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface MenuManageMapper { + List getMenuList(@Param("corpNo") String corpNo); + void insertMenu(MenuManageDto dto); + void updateMenu(MenuManageDto dto); + void deleteMauthByMenu(@Param("corpNo") String corpNo, @Param("menuNo") String menuNo); + void deleteMenu(@Param("corpNo") String corpNo, @Param("menuNo") String menuNo); + + List getAuthMenuList(Map params); + void mergeMenuAuth(AuthMenuDto dto); + void deleteMenuAuth(@Param("corpNo") String corpNo, + @Param("menuNo") String menuNo, + @Param("menuAuthCd") String menuAuthCd); + + List getUserMauthList(Map params); + void insertUserMauth(UserAuthDto dto); + void deleteUserMauth(@Param("corpNo") String corpNo, + @Param("usrId") String usrId, + @Param("menuAuthCd") String menuAuthCd); + + List> searchUser(Map params); +} diff --git a/backend/src/main/java/com/company/gw/envset/mapper/UserManageMapper.java b/backend/src/main/java/com/company/gw/envset/mapper/UserManageMapper.java new file mode 100644 index 0000000..c9901bd --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/mapper/UserManageMapper.java @@ -0,0 +1,26 @@ +package com.company.gw.envset.mapper; + +import com.company.gw.envset.dto.UserDetailDto; +import com.company.gw.envset.dto.UserListDto; +import com.company.gw.envset.dto.UserSaveDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface UserManageMapper { + List getUserList(Map params); + long getUserListCount(Map params); + UserDetailDto getUserDetail(Map params); + int getLoginIdCount(Map params); + void insertUser(UserSaveDto dto); + void updateUser(UserSaveDto dto); + void updateUserPw(@Param("corpNo") String corpNo, + @Param("usrId") String usrId, + @Param("pw") String pw, + @Param("modId") String modId); + void deleteUser(@Param("corpNo") String corpNo, @Param("usrId") String usrId); + void updateUserPhoto(Map params); +} diff --git a/backend/src/main/java/com/company/gw/envset/mapper/WorkCdMapper.java b/backend/src/main/java/com/company/gw/envset/mapper/WorkCdMapper.java new file mode 100644 index 0000000..d048711 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/mapper/WorkCdMapper.java @@ -0,0 +1,13 @@ +package com.company.gw.envset.mapper; + +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import java.util.Map; + +@Mapper +public interface WorkCdMapper { + List> getWorkCdList(Map param); + void insertWorkCd(Map param); + void updateWorkCd(Map param); + void deleteWorkCd(Map param); +} diff --git a/backend/src/main/java/com/company/gw/envset/service/CodeManageService.java b/backend/src/main/java/com/company/gw/envset/service/CodeManageService.java new file mode 100644 index 0000000..7db9d9b --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/service/CodeManageService.java @@ -0,0 +1,71 @@ +package com.company.gw.envset.service; + +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.exception.BizException; +import com.company.gw.envset.dto.CodeDto; +import com.company.gw.envset.dto.CodeIndexDto; +import com.company.gw.envset.mapper.CodeManageMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CodeManageService { + + private final CodeManageMapper codeManageMapper; + + public List getCdidxList(CurrentUser cu, String searchText) { + return codeManageMapper.getCdidxList(Map.of( + "corpNo", cu.getCorpNo(), + "searchText", searchText != null ? searchText : "" + )); + } + + @Transactional + public void saveCdidxList(CurrentUser cu, List list) { + for (CodeIndexDto item : list) { + item.setCorpNo(cu.getCorpNo()); + item.setRgstrId(cu.getUsrId()); + item.setModId(cu.getUsrId()); + + switch (item.getRowStatus()) { + case "I" -> codeManageMapper.insertCdidx(item); + case "U" -> codeManageMapper.updateCdidx(item); + case "D" -> codeManageMapper.deleteCdidx(cu.getCorpNo(), item.getCommClCd()); + default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus()); + } + } + } + + public List getCodeList(CurrentUser cu, String commClCd, String searchText) { + if (!StringUtils.hasText(commClCd)) throw new BizException("코드분류가 필요합니다."); + return codeManageMapper.getCodeList(Map.of( + "corpNo", cu.getCorpNo(), + "commClCd", commClCd, + "searchText", searchText != null ? searchText : "" + )); + } + + @Transactional + public void saveCodeList(CurrentUser cu, String commClCd, List list) { + if (!StringUtils.hasText(commClCd)) throw new BizException("코드분류가 필요합니다."); + for (CodeDto item : list) { + item.setCorpNo(cu.getCorpNo()); + item.setCommClCd(commClCd); + item.setRgstrId(cu.getUsrId()); + item.setModId(cu.getUsrId()); + + switch (item.getRowStatus()) { + case "I" -> codeManageMapper.insertCode(item); + case "U" -> codeManageMapper.updateCode(item); + case "D" -> codeManageMapper.deleteCode(cu.getCorpNo(), commClCd, item.getCommCd()); + default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus()); + } + } + } +} diff --git a/backend/src/main/java/com/company/gw/envset/service/MenuManageService.java b/backend/src/main/java/com/company/gw/envset/service/MenuManageService.java new file mode 100644 index 0000000..b6f3680 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/service/MenuManageService.java @@ -0,0 +1,102 @@ +package com.company.gw.envset.service; + +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.exception.BizException; +import com.company.gw.envset.dto.AuthMenuDto; +import com.company.gw.envset.dto.MenuManageDto; +import com.company.gw.envset.dto.UserAuthDto; +import com.company.gw.envset.mapper.MenuManageMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class MenuManageService { + + private final MenuManageMapper menuManageMapper; + + public List getMenuList(CurrentUser cu) { + return menuManageMapper.getMenuList(cu.getCorpNo()); + } + + @Transactional + public void saveMenuList(CurrentUser cu, List list) { + for (MenuManageDto item : list) { + item.setCorpNo(cu.getCorpNo()); + item.setRgstrId(cu.getUsrId()); + item.setModId(cu.getUsrId()); + + switch (item.getRowStatus()) { + case "I" -> menuManageMapper.insertMenu(item); + case "U" -> menuManageMapper.updateMenu(item); + case "D" -> { + menuManageMapper.deleteMauthByMenu(cu.getCorpNo(), item.getMenuNo()); + menuManageMapper.deleteMenu(cu.getCorpNo(), item.getMenuNo()); + } + default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus()); + } + } + } + + public List getAuthMenuList(CurrentUser cu, String menuAuthCd) { + if (!StringUtils.hasText(menuAuthCd)) throw new BizException("권한코드가 필요합니다."); + return menuManageMapper.getAuthMenuList(Map.of( + "corpNo", cu.getCorpNo(), + "menuAuthCd", menuAuthCd + )); + } + + @Transactional + public void saveAuthMenuList(CurrentUser cu, String menuAuthCd, List list) { + if (!StringUtils.hasText(menuAuthCd)) throw new BizException("권한코드가 필요합니다."); + for (AuthMenuDto item : list) { + if (!"U".equals(item.getRowStatus())) continue; + item.setCorpNo(cu.getCorpNo()); + item.setMenuAuthCd(menuAuthCd); + item.setRgstrId(cu.getUsrId()); + item.setModId(cu.getUsrId()); + + if ("Y".equals(item.getMenuAuthYn())) { + menuManageMapper.mergeMenuAuth(item); + } else { + menuManageMapper.deleteMenuAuth(cu.getCorpNo(), item.getMenuNo(), menuAuthCd); + } + } + } + + public List getUserMauthList(CurrentUser cu, String menuAuthCd, String usrNm) { + Map params = new HashMap<>(); + params.put("corpNo", cu.getCorpNo()); + params.put("menuAuthCd", StringUtils.hasText(menuAuthCd) ? menuAuthCd : null); + params.put("usrNm", StringUtils.hasText(usrNm) ? usrNm : null); + return menuManageMapper.getUserMauthList(params); + } + + @Transactional + public void saveUserMauthList(CurrentUser cu, List list) { + for (UserAuthDto item : list) { + item.setCorpNo(cu.getCorpNo()); + item.setRgstrId(cu.getUsrId()); + + switch (item.getRowStatus()) { + case "I" -> menuManageMapper.insertUserMauth(item); + case "D" -> menuManageMapper.deleteUserMauth( + cu.getCorpNo(), item.getUsrId(), item.getMenuAuthCd()); + default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus()); + } + } + } + + public List> searchUser(CurrentUser cu, String keyword) { + return menuManageMapper.searchUser(Map.of( + "corpNo", cu.getCorpNo(), + "keyword", keyword != null ? keyword : "" + )); + } +} diff --git a/backend/src/main/java/com/company/gw/envset/service/UserManageService.java b/backend/src/main/java/com/company/gw/envset/service/UserManageService.java new file mode 100644 index 0000000..ff39ccd --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/service/UserManageService.java @@ -0,0 +1,117 @@ +package com.company.gw.envset.service; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.common.exception.BizException; +import com.company.gw.common.service.SequenceService; +import com.company.gw.common.util.PasswordUtil; +import com.company.gw.envset.dto.UserDetailDto; +import com.company.gw.envset.dto.UserListDto; +import com.company.gw.envset.dto.UserSaveDto; +import com.company.gw.envset.mapper.UserManageMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class UserManageService { + + private static final List ALLOWED_ORDER_BY = + List.of("USR_ID", "USR_NM", "TEAM_CD", "DUTY_CD"); + + private final UserManageMapper userManageMapper; + private final SequenceService sequenceService; + + public Map getUserList(CurrentUser cu, Map params) { + String orderBy = (String) params.getOrDefault("userOrderBy", "USR_ID"); + if (!ALLOWED_ORDER_BY.contains(orderBy)) { + throw new BizException("정렬 조건이 올바르지 않습니다."); + } + params.put("corpNo", cu.getCorpNo()); + params.put("userOrderBy", orderBy); + + long total = userManageMapper.getUserListCount(params); + List list = userManageMapper.getUserList(params); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + return result; + } + + public UserDetailDto getUserDetail(CurrentUser cu, String usrId) { + Map params = Map.of("corpNo", cu.getCorpNo(), "usrId", usrId); + UserDetailDto detail = userManageMapper.getUserDetail(params); + if (detail == null) { + throw new BizException("사용자 정보가 없습니다."); + } + return detail; + } + + @Transactional + public void insertUser(CurrentUser cu, UserSaveDto dto) { + if (!StringUtils.hasText(dto.getPw())) { + throw new BizException("비밀번호는 필수입니다."); + } + dto.setCorpNo(cu.getCorpNo()); + dto.setRgstrId(cu.getUsrId()); + + // 시퀀스로 USR_ID 생성 (기존 getNextSeqString('SX_GW0010.USR_ID', 5)) + String usrId = sequenceService.getNextSeqString("SX_GW0010.USR_ID", 5); + dto.setUsrId(usrId); + + // 로그인ID 중복 체크 + checkLoginIdDuplicate(cu.getCorpNo(), dto.getLoginId(), usrId); + + // 비밀번호 암호화 (SHA-256 + Base64) + dto.setPw(PasswordUtil.encode(dto.getPw())); + + userManageMapper.insertUser(dto); + } + + @Transactional + public void updateUser(CurrentUser cu, UserSaveDto dto) { + if (!StringUtils.hasText(dto.getUsrId())) { + throw new BizException("USR_ID는 필수입니다."); + } + dto.setCorpNo(cu.getCorpNo()); + dto.setModId(cu.getUsrId()); + + // 로그인ID 중복 체크 (본인 제외) + checkLoginIdDuplicate(cu.getCorpNo(), dto.getLoginId(), dto.getUsrId()); + + userManageMapper.updateUser(dto); + + // 비밀번호 입력 시에만 변경 + if (StringUtils.hasText(dto.getPw())) { + userManageMapper.updateUserPw( + cu.getCorpNo(), dto.getUsrId(), + PasswordUtil.encode(dto.getPw()), cu.getUsrId()); + } + } + + @Transactional + public void deleteUser(CurrentUser cu, String usrId) { + // 사용자 존재 확인 + getUserDetail(cu, usrId); + userManageMapper.deleteUser(cu.getCorpNo(), usrId); + } + + private void checkLoginIdDuplicate(String corpNo, String loginId, String usrId) { + if (!StringUtils.hasText(loginId)) return; + Map params = new HashMap<>(); + params.put("corpNo", corpNo); + params.put("loginId", loginId); + params.put("usrId", usrId != null ? usrId : ""); + int cnt = userManageMapper.getLoginIdCount(params); + if (cnt > 0) { + throw new BizException("이미 사용 중인 로그인 아이디입니다."); + } + } +} diff --git a/backend/src/main/java/com/company/gw/envset/service/WorkCdService.java b/backend/src/main/java/com/company/gw/envset/service/WorkCdService.java new file mode 100644 index 0000000..2e0ae69 --- /dev/null +++ b/backend/src/main/java/com/company/gw/envset/service/WorkCdService.java @@ -0,0 +1,47 @@ +package com.company.gw.envset.service; + +import com.company.gw.envset.mapper.WorkCdMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class WorkCdService { + + private final WorkCdMapper workCdMapper; + + public List> getWorkCdList(String corpNo, String searchText) { + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("searchText", searchText); + return workCdMapper.getWorkCdList(param); + } + + @Transactional + public void insertWorkCd(String corpNo, String regUsrId, Map data) { + data.put("corpNo", corpNo); + data.put("regUsrId", regUsrId); + workCdMapper.insertWorkCd(data); + } + + @Transactional + public void updateWorkCd(String corpNo, String regUsrId, String workCd, Map data) { + data.put("corpNo", corpNo); + data.put("regUsrId", regUsrId); + data.put("workCd", workCd); + workCdMapper.updateWorkCd(data); + } + + @Transactional + public void deleteWorkCd(String corpNo, String workCd) { + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("workCd", workCd); + workCdMapper.deleteWorkCd(param); + } +} diff --git a/backend/src/main/java/com/company/gw/fedex/controller/FedexController.java b/backend/src/main/java/com/company/gw/fedex/controller/FedexController.java new file mode 100644 index 0000000..ba74172 --- /dev/null +++ b/backend/src/main/java/com/company/gw/fedex/controller/FedexController.java @@ -0,0 +1,58 @@ +package com.company.gw.fedex.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.fedex.service.FedexService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/fedex") +@RequiredArgsConstructor +public class FedexController { + + private final FedexService fedexService; + + /** 목록 */ + @GetMapping("/0010") + public ApiResponse> getFedexList( + @RequestParam(defaultValue = "") String searchText, + @RequestParam(defaultValue = "1") int pageNo, + @RequestParam(defaultValue = "100") int pageSize) { + return ApiResponse.ok(fedexService.getFedexList(searchText, pageNo, pageSize)); + } + + /** 상세 */ + @GetMapping("/0010/{sq}") + public ApiResponse> getFedexDetail(@PathVariable long sq) { + return ApiResponse.ok(fedexService.getFedexDetail(sq)); + } + + /** 등록 */ + @PostMapping("/0010") + public ApiResponse insertFedex( + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser cu) { + body.put("regUserId", cu.getUsrId()); + long sq = fedexService.insertFedex(body); + return ApiResponse.ok(sq); + } + + /** 첨부파일 번호 업데이트 */ + @PatchMapping("/0010/{sq}/attach") + public ApiResponse updateFedexAttach( + @PathVariable long sq, + @RequestBody Map body) { + fedexService.updateFedexAttach(sq, (String) body.get("attachNo")); + return ApiResponse.ok(null); + } + + /** 코드 목록 */ + @GetMapping("/codes") + public ApiResponse> getCodes() { + return ApiResponse.ok(fedexService.getCodes()); + } +} diff --git a/backend/src/main/java/com/company/gw/fedex/mapper/FedexMapper.java b/backend/src/main/java/com/company/gw/fedex/mapper/FedexMapper.java new file mode 100644 index 0000000..562f12a --- /dev/null +++ b/backend/src/main/java/com/company/gw/fedex/mapper/FedexMapper.java @@ -0,0 +1,17 @@ +package com.company.gw.fedex.mapper; + +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import java.util.Map; + +@Mapper +public interface FedexMapper { + int countFedexList(Map param); + List> getFedexList(Map param); + Map getFedexDetail(Map param); + void insertFedex(Map param); + void updateFedexAttach(Map param); + List> getFstList(); + List> getGwiList(); + List> getFjjList(); +} diff --git a/backend/src/main/java/com/company/gw/fedex/service/FedexService.java b/backend/src/main/java/com/company/gw/fedex/service/FedexService.java new file mode 100644 index 0000000..a0581c5 --- /dev/null +++ b/backend/src/main/java/com/company/gw/fedex/service/FedexService.java @@ -0,0 +1,60 @@ +package com.company.gw.fedex.service; + +import com.company.gw.fedex.mapper.FedexMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class FedexService { + + private final FedexMapper fedexMapper; + + public Map getFedexList(String searchText, int pageNo, int pageSize) { + Map param = new HashMap<>(); + param.put("searchText", searchText); + param.put("pageSize", pageSize); + param.put("offset", (pageNo - 1) * pageSize); + + int total = fedexMapper.countFedexList(param); + List> list = fedexMapper.getFedexList(param); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + return result; + } + + public Map getFedexDetail(long sq) { + Map param = new HashMap<>(); + param.put("sq", sq); + return fedexMapper.getFedexDetail(param); + } + + @Transactional + public long insertFedex(Map param) { + fedexMapper.insertFedex(param); + return ((Number) param.get("sq")).longValue(); + } + + @Transactional + public void updateFedexAttach(long sq, String attachNo) { + Map param = new HashMap<>(); + param.put("sq", sq); + param.put("attachNo", attachNo); + fedexMapper.updateFedexAttach(param); + } + + public Map getCodes() { + Map result = new HashMap<>(); + result.put("fstList", fedexMapper.getFstList()); + result.put("gwiList", fedexMapper.getGwiList()); + result.put("fjjList", fedexMapper.getFjjList()); + return result; + } +} diff --git a/backend/src/main/java/com/company/gw/main/controller/MainController.java b/backend/src/main/java/com/company/gw/main/controller/MainController.java new file mode 100644 index 0000000..34b339a --- /dev/null +++ b/backend/src/main/java/com/company/gw/main/controller/MainController.java @@ -0,0 +1,69 @@ +package com.company.gw.main.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.main.service.MainService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/main") +@RequiredArgsConstructor +public class MainController { + + private final MainService mainService; + + /** 대시보드 전체 데이터 */ + @GetMapping("/dashboard") + public ApiResponse> getDashboard( + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(mainService.getDashboard( + currentUser.getCorpNo(), currentUser.getUsrId())); + } + + /** 출근 */ + @PostMapping("/workstart") + public ApiResponse> clockIn( + @AuthenticationPrincipal CurrentUser currentUser, + @RequestBody Map body, + HttpServletRequest request) { + String date = body.get("workPlanYymmdd"); + String ip = getClientIp(request); + return ApiResponse.ok(mainService.clockIn( + currentUser.getCorpNo(), currentUser.getUsrId(), date, ip)); + } + + /** 퇴근 */ + @PostMapping("/workend") + public ApiResponse> clockOut( + @AuthenticationPrincipal CurrentUser currentUser, + @RequestBody Map body, + HttpServletRequest request) { + String date = body.get("workPlanYymmdd"); + String ip = getClientIp(request); + return ApiResponse.ok(mainService.clockOut( + currentUser.getCorpNo(), currentUser.getUsrId(), date, ip)); + } + + /** 오늘 지각자 목록 (관리자용) */ + @GetMapping("/late-list") + public ApiResponse>> getLateList( + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(mainService.getTodayLateList(currentUser.getCorpNo())); + } + + /** X-Forwarded-For → remoteAddr 순으로 클라이언트 IP 추출 */ + private String getClientIp(HttpServletRequest request) { + String xff = request.getHeader("X-Forwarded-For"); + if (StringUtils.hasText(xff)) { + return xff.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } +} diff --git a/backend/src/main/java/com/company/gw/main/mapper/MainMapper.java b/backend/src/main/java/com/company/gw/main/mapper/MainMapper.java new file mode 100644 index 0000000..d00d64e --- /dev/null +++ b/backend/src/main/java/com/company/gw/main/mapper/MainMapper.java @@ -0,0 +1,23 @@ +package com.company.gw.main.mapper; + +import org.apache.ibatis.annotations.Mapper; +import java.util.List; +import java.util.Map; + +@Mapper +public interface MainMapper { + List> getTopPostList(Map param); + List> getWorkRangeList(Map param); + Map getTodayWorkRecord(Map param); + Map getPendingApprCount(Map param); + List> getMyDocSentSummary(Map param); + List> getMyDocReceivedSummary(Map param); + List> getTodayWorkSummary(Map param); + + // 출퇴근 + Map getWorkplanInfo(Map param); + List getAllowedIpList(Map param); + int updateWorkStart(Map param); + int updateWorkEnd(Map param); + List> getTodayLateList(Map param); +} diff --git a/backend/src/main/java/com/company/gw/main/service/MainService.java b/backend/src/main/java/com/company/gw/main/service/MainService.java new file mode 100644 index 0000000..efa4524 --- /dev/null +++ b/backend/src/main/java/com/company/gw/main/service/MainService.java @@ -0,0 +1,221 @@ +package com.company.gw.main.service; + +import com.company.gw.common.exception.BizException; +import com.company.gw.main.mapper.MainMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class MainService { + + private final MainMapper mainMapper; + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyyMMdd"); + + /** 대시보드 전체 데이터 */ + public Map getDashboard(String corpNo, String usrId) { + LocalDate today = LocalDate.now(); + String todayStr = today.format(FMT); + String fromDate = today.minusDays(4).format(FMT); + String toDate = today.plusDays(4).format(FMT); + + Map result = new HashMap<>(); + + // 공지사항 최신 5건 (UNTY_BBS_CD='0002') + result.put("noticeList", getTopPostList(corpNo, "0002", 5)); + // 자유게시판 최신 5건 (UNTY_BBS_CD='0001') + result.put("boardList", getTopPostList(corpNo, "0001", 5)); + // 업무메뉴얼 최신 5건 (UNTY_BBS_CD='0004') + result.put("manualList", getTopPostList(corpNo, "0004", 5)); + + // 9일 근무 범위 (오늘±4일) + Map wp = new HashMap<>(); + wp.put("corpNo", corpNo); + wp.put("usrId", usrId); + wp.put("fromDate", fromDate); + wp.put("toDate", toDate); + result.put("workRangeList", mainMapper.getWorkRangeList(wp)); + + // 오늘 근무 기록 + result.put("todayWork", getTodayWorkRecord(corpNo, usrId, todayStr)); + + // 결재 대기 건수 + result.put("pendingAppr", getPendingApprCount(corpNo, usrId)); + + // 내가 올린 결재 문서 상태별 카운트 + Map sp = new HashMap<>(); + sp.put("corpNo", corpNo); + sp.put("usrId", usrId); + result.put("myDocSent", mainMapper.getMyDocSentSummary(sp)); + + // 내가 받은 결재 문서 상태별 카운트 + result.put("myDocReceived", mainMapper.getMyDocReceivedSummary(sp)); + + // 오늘 전체 근무 현황 (근무코드별 그룹) + Map ts = new HashMap<>(); + ts.put("corpNo", corpNo); + ts.put("today", todayStr); + result.put("todayWorkSummary", mainMapper.getTodayWorkSummary(ts)); + + // 오늘 지각자 + result.put("todayLateList", getTodayLateList(corpNo)); + + return result; + } + + private List> getTopPostList(String corpNo, String bbsCd, int top) { + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("bbsCd", bbsCd); + param.put("top", top); + return mainMapper.getTopPostList(param); + } + + private Map getTodayWorkRecord(String corpNo, String usrId, String today) { + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("usrId", usrId); + param.put("today", today); + return mainMapper.getTodayWorkRecord(param); + } + + private Map getPendingApprCount(String corpNo, String usrId) { + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("usrId", usrId); + return mainMapper.getPendingApprCount(param); + } + + // ───────────────────────────────────────────────────── + // 출퇴근 + // ───────────────────────────────────────────────────── + + /** 근무계획 단건 조회 */ + public Map getWorkplanInfo(String corpNo, String usrId, String workPlanYymmdd) { + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("usrId", usrId); + param.put("workPlanYymmdd", workPlanYymmdd); + return mainMapper.getWorkplanInfo(param); + } + + /** IP 허용 여부 검사 (SX014 코드에 IP 등록 없으면 전체 허용) */ + public void checkAllowedIp(String corpNo, String clientIp) { + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + List ipList = mainMapper.getAllowedIpList(param); + if (ipList == null || ipList.isEmpty()) return; // 설정 없으면 전체 허용 + for (String allowed : ipList) { + if (StringUtils.hasText(allowed) && clientIp.startsWith(allowed.trim())) return; + } + throw new BizException("접속 IP(" + clientIp + ")가 허용 IP가 아닙니다.", HttpStatus.FORBIDDEN); + } + + /** 출근 처리 */ + @Transactional + public Map clockIn(String corpNo, String usrId, String workPlanYymmdd, String clientIp) { + checkAllowedIp(corpNo, clientIp); + + Map wp = getWorkplanInfo(corpNo, usrId, workPlanYymmdd); + if (wp == null || !StringUtils.hasText((String) wp.get("REAL_WORK_CD"))) { + throw new BizException(workPlanYymmdd + " 일의 근무 계획이 없습니다."); + } + if (StringUtils.hasText((String) wp.get("WORK_START_DT"))) { + throw new BizException(workPlanYymmdd + " 에 이미 출근 기록이 있습니다."); + } + + // 지각 여부 계산 + int lateMin = calcLateMin((String) wp.get("GOTOWORK_TM_NM")); + + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("usrId", usrId); + param.put("workPlanYymmdd", workPlanYymmdd); + mainMapper.updateWorkStart(param); + + Map result = new HashMap<>(); + result.put("lateMin", lateMin); + return result; + } + + /** 퇴근 처리 */ + @Transactional + public Map clockOut(String corpNo, String usrId, String workPlanYymmdd, String clientIp) { + checkAllowedIp(corpNo, clientIp); + + Map wp = getWorkplanInfo(corpNo, usrId, workPlanYymmdd); + if (wp == null || !StringUtils.hasText((String) wp.get("REAL_WORK_CD"))) { + throw new BizException(workPlanYymmdd + " 일의 근무 계획이 없습니다."); + } + if (!StringUtils.hasText((String) wp.get("WORK_START_DT"))) { + throw new BizException(workPlanYymmdd + " 에 출근 기록이 없습니다."); + } + if (StringUtils.hasText((String) wp.get("WORK_END_DT"))) { + throw new BizException(workPlanYymmdd + " 에 이미 퇴근 기록이 있습니다."); + } + + // 조퇴 여부 계산 + int earlyMin = calcEarlyMin((String) wp.get("GETOFFWORK_TM_NM")); + + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("usrId", usrId); + param.put("workPlanYymmdd", workPlanYymmdd); + mainMapper.updateWorkEnd(param); + + Map result = new HashMap<>(); + result.put("earlyMin", earlyMin); + return result; + } + + /** 오늘 지각자 목록 */ + @Transactional(readOnly = true) + public List> getTodayLateList(String corpNo) { + String today = LocalDate.now().format(FMT); + Map param = new HashMap<>(); + param.put("corpNo", corpNo); + param.put("today", today); + return mainMapper.getTodayLateList(param); + } + + /** 지각 분 계산 (GOTOWORK_TM_NM 예: "0900") */ + private int calcLateMin(String gotoworkTmNm) { + if (!StringUtils.hasText(gotoworkTmNm) || gotoworkTmNm.length() < 4) return 0; + try { + LocalTime scheduled = LocalTime.of( + Integer.parseInt(gotoworkTmNm.substring(0, 2)), + Integer.parseInt(gotoworkTmNm.substring(2, 4)) + ); + LocalTime now = LocalTime.now(); + if (now.isAfter(scheduled)) { + return (int) java.time.Duration.between(scheduled, now).toMinutes(); + } + } catch (Exception ignored) {} + return 0; + } + + /** 조퇴 분 계산 (GETOFFWORK_TM_NM 예: "1800") */ + private int calcEarlyMin(String getoffworkTmNm) { + if (!StringUtils.hasText(getoffworkTmNm) || getoffworkTmNm.length() < 4) return 0; + try { + LocalTime scheduled = LocalTime.of( + Integer.parseInt(getoffworkTmNm.substring(0, 2)), + Integer.parseInt(getoffworkTmNm.substring(2, 4)) + ); + LocalTime now = LocalTime.now(); + if (now.isBefore(scheduled)) { + return (int) java.time.Duration.between(now, scheduled).toMinutes(); + } + } catch (Exception ignored) {} + return 0; + } +} diff --git a/backend/src/main/java/com/company/gw/tam/controller/TamController.java b/backend/src/main/java/com/company/gw/tam/controller/TamController.java new file mode 100644 index 0000000..e160749 --- /dev/null +++ b/backend/src/main/java/com/company/gw/tam/controller/TamController.java @@ -0,0 +1,193 @@ +package com.company.gw.tam.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.tam.service.TamService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/tam") +@RequiredArgsConstructor +public class TamController { + + private final TamService tamService; + + // ────────────────────────────────────────────── + // TAM0010 - 연차 관리 + // ────────────────────────────────────────────── + + @GetMapping("/yyvct") + public ApiResponse>> getYyvctList( + @RequestParam String yyvctYy, + @RequestParam(defaultValue = "") String usrNm, + @RequestParam(defaultValue = "") String teamCd, + @RequestParam(defaultValue = "N") String includeRetireYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(tamService.getYyvctList( + currentUser.getCorpNo(), yyvctYy, usrNm, teamCd, includeRetireYn)); + } + + @PostMapping("/yyvct") + public ApiResponse saveYyvct( + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + tamService.saveYyvct( + currentUser.getCorpNo(), + (String) body.get("yyvctYy"), + (String) body.get("usrId"), + Integer.parseInt(body.get("yyvctCnt").toString())); + return ApiResponse.ok(null); + } + + @DeleteMapping("/yyvct") + public ApiResponse deleteYyvct( + @RequestParam String yyvctYy, + @RequestParam String usrId, + @AuthenticationPrincipal CurrentUser currentUser) { + tamService.deleteYyvct(currentUser.getCorpNo(), yyvctYy, usrId); + return ApiResponse.ok(null); + } + + // ────────────────────────────────────────────── + // TAM0020 - 결재 신청 + // ────────────────────────────────────────────── + + @GetMapping("/apvreq") + public ApiResponse> getApvreqList( + @RequestParam Map params, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(tamService.getApvreqList(currentUser.getCorpNo(), params)); + } + + @GetMapping("/apvreq/{aprvlDocId}") + public ApiResponse> getApvreqDetail( + @PathVariable String aprvlDocId, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(tamService.getApvreqDetail(currentUser.getCorpNo(), aprvlDocId)); + } + + @PostMapping("/apvreq") + public ApiResponse> createApvreq( + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + String aprvlDocId = tamService.createApvreq(currentUser.getCorpNo(), body); + return ApiResponse.ok(Map.of("aprvlDocId", aprvlDocId)); + } + + @PutMapping("/apvreq/{aprvlDocId}") + public ApiResponse updateApvreq( + @PathVariable String aprvlDocId, + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + tamService.updateApvreq(currentUser.getCorpNo(), aprvlDocId, body); + return ApiResponse.ok(null); + } + + @PostMapping("/apvreq/{aprvlDocId}/request") + public ApiResponse requestApvreq( + @PathVariable String aprvlDocId, + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + @SuppressWarnings("unchecked") + List> apprList = (List>) body.get("apprList"); + tamService.requestApvreq(currentUser.getCorpNo(), aprvlDocId, apprList); + return ApiResponse.ok(null); + } + + @DeleteMapping("/apvreq/{aprvlDocId}") + public ApiResponse deleteApvreq( + @PathVariable String aprvlDocId, + @AuthenticationPrincipal CurrentUser currentUser) { + tamService.deleteApvreq(currentUser.getCorpNo(), aprvlDocId); + return ApiResponse.ok(null); + } + + @GetMapping("/apvreq/latest-appr") + public ApiResponse>> getLatestApprList( + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(tamService.getLatestApprList(currentUser.getCorpNo())); + } + + // ────────────────────────────────────────────── + // TAM0030 - 결재 처리 + // ────────────────────────────────────────────── + + @GetMapping("/apvapp") + public ApiResponse> getApvappList( + @RequestParam Map params, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(tamService.getApvappList(currentUser.getCorpNo(), params)); + } + + @GetMapping("/apvapp/{aprvlDocId}") + public ApiResponse> getApvappDetail( + @PathVariable String aprvlDocId, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(tamService.getApvappDetail(currentUser.getCorpNo(), aprvlDocId)); + } + + @PostMapping("/apvapp/{aprvlDocId}/approve") + public ApiResponse approve( + @PathVariable String aprvlDocId, + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + tamService.approveApvdoc(currentUser.getCorpNo(), aprvlDocId, + Integer.parseInt(body.get("aprvlSno").toString()), + (String) body.get("apprCn")); + return ApiResponse.ok(null); + } + + @PostMapping("/apvapp/{aprvlDocId}/reject") + public ApiResponse reject( + @PathVariable String aprvlDocId, + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + tamService.rejectApvdoc(currentUser.getCorpNo(), aprvlDocId, + Integer.parseInt(body.get("aprvlSno").toString()), + (String) body.get("apprCn")); + return ApiResponse.ok(null); + } + + /** 일괄 승인 */ + @PostMapping("/apvapp/multi-approve") + public ApiResponse multiApprove( + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + @SuppressWarnings("unchecked") + List> items = (List>) body.get("items"); + tamService.multiApprove(currentUser.getCorpNo(), items, (String) body.get("apprCn")); + return ApiResponse.ok(null); + } + + /** 일괄 반려 */ + @PostMapping("/apvapp/multi-reject") + public ApiResponse multiReject( + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + @SuppressWarnings("unchecked") + List> items = (List>) body.get("items"); + tamService.multiReject(currentUser.getCorpNo(), items, (String) body.get("apprCn")); + return ApiResponse.ok(null); + } + + // ────────────────────────────────────────────── + // TAM0040 - 근태 현황 + // ────────────────────────────────────────────── + + @GetMapping("/status") + public ApiResponse>> getTamStatusList( + @RequestParam String yyvctYy, + @RequestParam(defaultValue = "") String teamCd, + @RequestParam(defaultValue = "00000000") String staYmd, + @RequestParam(defaultValue = "99999999") String endYmd, + @RequestParam(defaultValue = "N") String includeRetireYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(tamService.getTamStatusList( + currentUser.getCorpNo(), yyvctYy, teamCd, staYmd, endYmd, includeRetireYn)); + } +} diff --git a/backend/src/main/java/com/company/gw/tam/mapper/ApvdocMapper.java b/backend/src/main/java/com/company/gw/tam/mapper/ApvdocMapper.java new file mode 100644 index 0000000..a453ccd --- /dev/null +++ b/backend/src/main/java/com/company/gw/tam/mapper/ApvdocMapper.java @@ -0,0 +1,52 @@ +package com.company.gw.tam.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface ApvdocMapper { + + Map getApvdocInfo(Map param); + + List> getApprList(Map param); + + Map getApprInfo(Map param); + + void insertApvdoc(Map param); + + void updateApvdocContent(Map param); + + void updateApvdocAtchNo(Map param); + + void updateApvdocStatusRequest(Map param); + + void updateApvdocStatusApprove(Map param); + + void updateApvdocStatusReject(Map param); + + void updateFinalAprvlSno(Map param); + + void updateAprvlCmplSno(Map param); + + void deleteApprAll(Map param); + + void insertAppr(Map param); + + void updateApprStatus(Map param); + + void apprApproveOrReject(Map param); + + void updateNextApprStatusAppring(Map param); + + void deleteApvdocSx0080(Map param); + + void deleteApvdocSx0110(Map param); + + void deleteApvdocSx0090(Map param); + + void updateApvdocDocFlagAplnt(Map param); + + void resetWorkEndDt(Map param); +} diff --git a/backend/src/main/java/com/company/gw/tam/mapper/TamMapper.java b/backend/src/main/java/com/company/gw/tam/mapper/TamMapper.java new file mode 100644 index 0000000..6e5284b --- /dev/null +++ b/backend/src/main/java/com/company/gw/tam/mapper/TamMapper.java @@ -0,0 +1,43 @@ +package com.company.gw.tam.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface TamMapper { + + // TAM0010 - 연차 + List> getYyvctList(Map param); + void upsertYyvct(Map param); + void deleteYyvct(Map param); + + // TAM0020 - 결재 신청 + List> getApvreqList(Map param); + long getApvreqListCount(Map param); + List> getWorkChangeList(Map param); + Map getOtInfo(Map param); + void insertSxGw0110(Map param); + void insertSxGw0080(Map param); + void deleteSxGw0110(Map param); + void deleteSxGw0080(Map param); + List> getLatestApprList(Map param); + void updateDocFlagAplnt(Map param); + + // TAM0030 - 결재 처리 + List> getApvappList(Map param); + long getApvappListCount(Map param); + void updateDocFlagAppr(Map param); + void updateOutingTime(Map param); + + // TAM0040 - 현황/통계 + List> getTamStatusList(Map param); + + // 결재 후처리 + int afterProcessOt(Map param); + int afterProcessLate(Map param); + int afterProcessEarlyDep(Map param); + int afterProcessAbsence(Map param); + int afterProcessWorkChange(Map param); +} diff --git a/backend/src/main/java/com/company/gw/tam/service/ApvdocService.java b/backend/src/main/java/com/company/gw/tam/service/ApvdocService.java new file mode 100644 index 0000000..695a3e0 --- /dev/null +++ b/backend/src/main/java/com/company/gw/tam/service/ApvdocService.java @@ -0,0 +1,262 @@ +package com.company.gw.tam.service; + +import com.company.gw.common.exception.BizException; +import com.company.gw.common.service.AttachService; +import com.company.gw.common.service.SequenceService; +import com.company.gw.common.util.SecurityUtil; +import com.company.gw.tam.mapper.ApvdocMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 결재 공통 서비스 + * 결재 상태코드: 0001=작성중, 0002=결재중, 0003=결재완료, 0004=반려 + */ +@Service +@RequiredArgsConstructor +public class ApvdocService { + + public static final String STATUS_WRITING = "0001"; + public static final String STATUS_APPRING = "0002"; + public static final String STATUS_APPROVED = "0003"; + public static final String STATUS_REJECTED = "0004"; + + private final ApvdocMapper apvdocMapper; + private final AttachService attachService; + private final SequenceService sequenceService; + + // ────────────────────────────────────────────── + // 결재문서 조회 + // ────────────────────────────────────────────── + + public Map getApvdocInfo(String corpNo, String aprvlDocId) { + Map info = apvdocMapper.getApvdocInfo(param(corpNo, aprvlDocId)); + if (info == null) throw new BizException("결재문서가 존재하지 않습니다.", HttpStatus.NOT_FOUND); + return info; + } + + public List> getApprList(String corpNo, String aprvlDocId) { + return apvdocMapper.getApprList(param(corpNo, aprvlDocId)); + } + + /** 결재문서 + 결재자목록 + 첨부파일 통합 조회 */ + @Transactional(readOnly = true) + public Map getApvdocInfoAll(String corpNo, String aprvlDocId) { + Map info = getApvdocInfo(corpNo, aprvlDocId); + info.put("apprList", getApprList(corpNo, aprvlDocId)); + String atchNo = (String) info.get("ATCH_NO"); + if (atchNo != null) { + info.put("attachFileList", attachService.getFileList(atchNo)); + } + return info; + } + + // ────────────────────────────────────────────── + // 결재문서 생성 + // ────────────────────────────────────────────── + + @Transactional + public String createApvdoc(String corpNo, String aprvlKindCd, + String aplntCn, String bzCn, String offerCn, String bzDeputyId, + String atchNo) { + String usrId = SecurityUtil.getUsrId(); + String aprvlDocId = sequenceService.getNextSeqString("SX_GW0090.APRVL_DOC_ID", 10); + + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("aprvlDocId", aprvlDocId); + p.put("aprvlKindCd", aprvlKindCd); + p.put("aplntCn", aplntCn); + p.put("bzCn", bzCn); + p.put("offerCn", offerCn); + p.put("bzDeputyId", bzDeputyId); + p.put("atchNo", atchNo); + p.put("usrId", usrId); + + apvdocMapper.insertApvdoc(p); + + if (org.springframework.util.StringUtils.hasText(atchNo)) { + attachService.confirmAtchNo(atchNo, "SX_GW0090.ATCH_NO"); + } + + return aprvlDocId; + } + + @Transactional + public void updateApvdocContent(String corpNo, String aprvlDocId, + String aplntCn, String bzCn, String offerCn, String bzDeputyId, + String atchNo) { + String usrId = SecurityUtil.getUsrId(); + Map info = getApvdocInfo(corpNo, aprvlDocId); + checkAplnt(info); + checkBeforeRequest(info); + + Map p = param(corpNo, aprvlDocId); + p.put("aplntCn", aplntCn); + p.put("bzCn", bzCn); + p.put("offerCn", offerCn); + p.put("bzDeputyId", bzDeputyId); + p.put("usrId", usrId); + apvdocMapper.updateApvdocContent(p); + + if (org.springframework.util.StringUtils.hasText(atchNo)) { + Map ap = param(corpNo, aprvlDocId); + ap.put("atchNo", atchNo); + apvdocMapper.updateApvdocAtchNo(ap); + attachService.confirmAtchNo(atchNo, "SX_GW0090.ATCH_NO"); + } + } + + // ────────────────────────────────────────────── + // 결재자 등록 + // ────────────────────────────────────────────── + + @Transactional + public void insertApprList(String corpNo, String aprvlDocId, List> apprList) { + String usrId = SecurityUtil.getUsrId(); + Map info = getApvdocInfo(corpNo, aprvlDocId); + checkBeforeRequest(info); + + apvdocMapper.deleteApprAll(param(corpNo, aprvlDocId)); + + int sno = 1; + for (Map appr : apprList) { + String apprId = (String) appr.get("apprId"); + if (apprId == null || apprId.isEmpty()) { + throw new BizException("결재자가 지정되지 않았습니다."); + } + Map p = param(corpNo, aprvlDocId); + p.put("aprvlSno", sno++); + p.put("apprId", apprId); + p.put("usrId", usrId); + apvdocMapper.insertAppr(p); + } + apvdocMapper.updateFinalAprvlSno(param(corpNo, aprvlDocId)); + } + + // ────────────────────────────────────────────── + // 결재 상신 + // ────────────────────────────────────────────── + + @Transactional + public void requestApvdoc(String corpNo, String aprvlDocId) { + String usrId = SecurityUtil.getUsrId(); + Map info = getApvdocInfo(corpNo, aprvlDocId); + checkBeforeRequest(info); + + List> apprList = getApprList(corpNo, aprvlDocId); + if (apprList.isEmpty()) throw new BizException("결재자가 지정되지 않았습니다."); + + // 1차 결재자 → 결재중 + Map p1 = param(corpNo, aprvlDocId); + p1.put("aprvlSno", 1); + p1.put("apprStusCd", STATUS_APPRING); + p1.put("usrId", usrId); + apvdocMapper.updateApprStatus(p1); + + // 문서 → 결재중 + apvdocMapper.updateApvdocStatusRequest(param(corpNo, aprvlDocId)); + } + + // ────────────────────────────────────────────── + // 승인 / 반려 + // ────────────────────────────────────────────── + + @Transactional + public void approveApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) { + processApproveOrReject(corpNo, aprvlDocId, aprvlSno, apprCn, true); + } + + @Transactional + public void rejectApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) { + processApproveOrReject(corpNo, aprvlDocId, aprvlSno, apprCn, false); + } + + private void processApproveOrReject(String corpNo, String aprvlDocId, + int aprvlSno, String apprCn, boolean isApprove) { + String usrId = SecurityUtil.getUsrId(); + + Map docInfo = getApvdocInfo(corpNo, aprvlDocId); + if (!STATUS_APPRING.equals(docInfo.get("APRVL_STUS_CD"))) { + throw new BizException("결재중 상태가 아닙니다."); + } + + Map apprParam = param(corpNo, aprvlDocId); + apprParam.put("aprvlSno", aprvlSno); + Map apprInfo = apvdocMapper.getApprInfo(apprParam); + + if (apprInfo == null) throw new BizException("결재자 정보가 없습니다. (" + aprvlSno + "차)"); + if (!STATUS_APPRING.equals(apprInfo.get("APPR_STUS_CD"))) throw new BizException("결재중인 상태가 아닙니다."); + if (!usrId.equals(apprInfo.get("APPR_ID"))) throw new BizException("결재자가 아닙니다."); + + String apprStusCd = isApprove ? STATUS_APPROVED : STATUS_REJECTED; + Map p = param(corpNo, aprvlDocId); + p.put("aprvlSno", aprvlSno); + p.put("apprStusCd", apprStusCd); + p.put("apprCn", apprCn); + p.put("usrId", usrId); + apvdocMapper.apprApproveOrReject(p); + + if (isApprove) { + if ("Y".equals(apprInfo.get("FINAL_APPR_YN"))) { + apvdocMapper.updateApvdocStatusApprove(param(corpNo, aprvlDocId)); + } else { + Map nextP = param(corpNo, aprvlDocId); + nextP.put("aprvlSno", aprvlSno); + nextP.put("usrId", usrId); + apvdocMapper.updateNextApprStatusAppring(nextP); + } + } else { + apvdocMapper.updateApvdocStatusReject(param(corpNo, aprvlDocId)); + apvdocMapper.resetWorkEndDt(param(corpNo, aprvlDocId)); + } + apvdocMapper.updateAprvlCmplSno(param(corpNo, aprvlDocId)); + } + + // ────────────────────────────────────────────── + // 결재문서 삭제 + // ────────────────────────────────────────────── + + @Transactional + public void deleteApvdoc(String corpNo, String aprvlDocId) { + Map info = getApvdocInfo(corpNo, aprvlDocId); + checkAplnt(info); + Map p = param(corpNo, aprvlDocId); + apvdocMapper.deleteApvdocSx0080(p); + apvdocMapper.deleteApvdocSx0110(p); + apvdocMapper.deleteApvdocSx0090(p); + + String atchNo = (String) info.get("ATCH_NO"); + if (atchNo != null) attachService.deleteFilesByAtchNo(atchNo, true); + } + + // ────────────────────────────────────────────── + // 내부 유틸 + // ────────────────────────────────────────────── + + private Map param(String corpNo, String aprvlDocId) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("aprvlDocId", aprvlDocId); + return p; + } + + private void checkBeforeRequest(Map docInfo) { + if (!STATUS_WRITING.equals(docInfo.get("APRVL_STUS_CD"))) { + throw new BizException("작성중 상태가 아닙니다."); + } + } + + private void checkAplnt(Map docInfo) { + String usrId = SecurityUtil.getUsrId(); + if (!usrId.equals(docInfo.get("APLNT_ID"))) { + throw new BizException("신청자가 아닙니다.", HttpStatus.FORBIDDEN); + } + } +} diff --git a/backend/src/main/java/com/company/gw/tam/service/TamService.java b/backend/src/main/java/com/company/gw/tam/service/TamService.java new file mode 100644 index 0000000..36057e9 --- /dev/null +++ b/backend/src/main/java/com/company/gw/tam/service/TamService.java @@ -0,0 +1,315 @@ +package com.company.gw.tam.service; + +import com.company.gw.common.util.SecurityUtil; +import com.company.gw.tam.mapper.TamMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class TamService { + + private final TamMapper tamMapper; + private final ApvdocService apvdocService; + + // ────────────────────────────────────────────── + // TAM0010 - 연차 관리 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getYyvctList(String corpNo, String yyvctYy, + String usrNm, String teamCd, + String includeRetireYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("yyvctYy", yyvctYy); + p.put("usrNm", usrNm); + p.put("teamCd", teamCd); + p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N"); + return tamMapper.getYyvctList(p); + } + + @Transactional + public void saveYyvct(String corpNo, String yyvctYy, String usrId, int yyvctCnt) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("yyvctYy", yyvctYy); + p.put("usrId", usrId); + p.put("yyvctCnt", yyvctCnt); + p.put("loginUsrId", SecurityUtil.getUsrId()); + tamMapper.upsertYyvct(p); + } + + @Transactional + public void deleteYyvct(String corpNo, String yyvctYy, String usrId) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("yyvctYy", yyvctYy); + p.put("usrId", usrId); + tamMapper.deleteYyvct(p); + } + + // ────────────────────────────────────────────── + // TAM0020 - 결재 신청 목록 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public Map getApvreqList(String corpNo, Map filter) { + filter.put("corpNo", corpNo); + filter.put("usrId", SecurityUtil.getUsrId()); + filter.put("pageNo", Integer.parseInt(filter.getOrDefault("pageNo", "1").toString())); + filter.put("pageSize", Integer.parseInt(filter.getOrDefault("pageSize", "20").toString())); + if (!filter.containsKey("staYmd") || filter.get("staYmd") == null || filter.get("staYmd").toString().isEmpty()) + filter.put("staYmd", "00000000"); + if (!filter.containsKey("endYmd") || filter.get("endYmd") == null || filter.get("endYmd").toString().isEmpty()) + filter.put("endYmd", "99999999"); + + List> list = tamMapper.getApvreqList(filter); + long total = tamMapper.getApvreqListCount(filter); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + return result; + } + + /** 결재 신청 상세 (문서 + 결재자 + 첨부 + 세부항목) */ + @Transactional(readOnly = true) + public Map getApvreqDetail(String corpNo, String aprvlDocId) { + Map info = apvdocService.getApvdocInfoAll(corpNo, aprvlDocId); + + // 변경근무 목록 + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("aprvlDocId", aprvlDocId); + info.put("workChangeList", tamMapper.getWorkChangeList(p)); + info.put("otInfo", tamMapper.getOtInfo(p)); + + return info; + } + + /** 결재 신청 생성 */ + @Transactional + public String createApvreq(String corpNo, Map body) { + String aprvlDocId = apvdocService.createApvdoc( + corpNo, + (String) body.get("aprvlKindCd"), + (String) body.get("aplntCn"), + (String) body.get("bzCn"), + (String) body.get("offerCn"), + (String) body.get("bzDeputyId"), + (String) body.get("atchNo") + ); + + // 세부 항목 등록 + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("aprvlDocId", aprvlDocId); + p.put("usrId", SecurityUtil.getUsrId()); + + @SuppressWarnings("unchecked") + List> workChangeList = (List>) body.get("workChangeList"); + if (workChangeList != null) { + for (Map item : workChangeList) { + Map ip = new HashMap<>(p); + ip.putAll(item); + tamMapper.insertSxGw0110(ip); + } + } + + Map otInfo = (Map) body.get("otInfo"); + if (otInfo != null) { + Map ip = new HashMap<>(p); + ip.putAll(otInfo); + tamMapper.insertSxGw0080(ip); + } + + return aprvlDocId; + } + + /** 결재 신청 수정 */ + @Transactional + public void updateApvreq(String corpNo, String aprvlDocId, Map body) { + apvdocService.updateApvdocContent(corpNo, aprvlDocId, + (String) body.get("aplntCn"), (String) body.get("bzCn"), + (String) body.get("offerCn"), (String) body.get("bzDeputyId"), + (String) body.get("atchNo")); + + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("aprvlDocId", aprvlDocId); + p.put("usrId", SecurityUtil.getUsrId()); + + tamMapper.deleteSxGw0110(p); + tamMapper.deleteSxGw0080(p); + + @SuppressWarnings("unchecked") + List> workChangeList = (List>) body.get("workChangeList"); + if (workChangeList != null) { + for (Map item : workChangeList) { + Map ip = new HashMap<>(p); + ip.putAll(item); + tamMapper.insertSxGw0110(ip); + } + } + Map otInfo = (Map) body.get("otInfo"); + if (otInfo != null) { + Map ip = new HashMap<>(p); + ip.putAll(otInfo); + tamMapper.insertSxGw0080(ip); + } + } + + /** 결재 상신 (결재자 등록 + 상신) */ + @Transactional + public void requestApvreq(String corpNo, String aprvlDocId, List> apprList) { + apvdocService.insertApprList(corpNo, aprvlDocId, apprList); + apvdocService.requestApvdoc(corpNo, aprvlDocId); + } + + /** 결재문서 삭제 */ + @Transactional + public void deleteApvreq(String corpNo, String aprvlDocId) { + apvdocService.deleteApvdoc(corpNo, aprvlDocId); + } + + /** 최근 결재자 목록 조회 */ + @Transactional(readOnly = true) + public List> getLatestApprList(String corpNo) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("usrId", SecurityUtil.getUsrId()); + return tamMapper.getLatestApprList(p); + } + + // ────────────────────────────────────────────── + // TAM0030 - 결재 처리 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public Map getApvappList(String corpNo, Map filter) { + filter.put("corpNo", corpNo); + filter.put("usrId", SecurityUtil.getUsrId()); + filter.put("pageNo", Integer.parseInt(filter.getOrDefault("pageNo", "1").toString())); + filter.put("pageSize", Integer.parseInt(filter.getOrDefault("pageSize", "20").toString())); + + List> list = tamMapper.getApvappList(filter); + long total = tamMapper.getApvappListCount(filter); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + return result; + } + + /** 결재 처리 상세 */ + @Transactional(readOnly = true) + public Map getApvappDetail(String corpNo, String aprvlDocId) { + return apvdocService.getApvdocInfoAll(corpNo, aprvlDocId); + } + + /** 승인 처리 (최종 승인 시 executeAfterProcess 호출) */ + @Transactional + public void approveApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) { + // 승인 전 문서 정보 (aplntId, aprvlKindCd 필요) + Map docBefore = apvdocService.getApvdocInfo(corpNo, aprvlDocId); + apvdocService.approveApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn); + // 최종 승인 여부 확인 + Map docAfter = apvdocService.getApvdocInfo(corpNo, aprvlDocId); + if (ApvdocService.STATUS_APPROVED.equals(docAfter.get("APRVL_STUS_CD"))) { + executeAfterProcess(corpNo, aprvlDocId, + (String) docBefore.get("APRVL_KIND_CD"), + (String) docBefore.get("APLNT_ID")); + } + } + + /** 반려 처리 */ + @Transactional + public void rejectApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) { + apvdocService.rejectApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn); + } + + /** 일괄 승인 */ + @Transactional + public void multiApprove(String corpNo, List> items, String apprCn) { + for (Map item : items) { + String aprvlDocId = (String) item.get("aprvlDocId"); + int aprvlSno = Integer.parseInt(item.get("aprvlSno").toString()); + try { + approveApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn); + } catch (Exception e) { + // 일괄 처리 중 개별 실패는 무시하고 계속 진행 + } + } + } + + /** 일괄 반려 */ + @Transactional + public void multiReject(String corpNo, List> items, String apprCn) { + for (Map item : items) { + String aprvlDocId = (String) item.get("aprvlDocId"); + int aprvlSno = Integer.parseInt(item.get("aprvlSno").toString()); + try { + rejectApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn); + } catch (Exception e) { + // 일괄 처리 중 개별 실패는 무시하고 계속 진행 + } + } + } + + /** 최종 승인 후처리 - 결재 종류별 근무 데이터 업데이트 */ + private void executeAfterProcess(String corpNo, String aprvlDocId, + String aprvlKindCd, String aplntId) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("aprvlDocId", aprvlDocId); + p.put("aplntId", aplntId); + + switch (aprvlKindCd != null ? aprvlKindCd : "") { + case "0001": // 시간외 → OT_RCTN_YN='Y' + tamMapper.afterProcessOt(p); + break; + case "0002": // 지각 → WORK_START_DT 보정 + tamMapper.afterProcessLate(p); + break; + case "0003": // 조퇴 → WORK_END_DT 보정 + EL_RCTN_YN='Y' + tamMapper.afterProcessEarlyDep(p); + break; + case "0004": // 결근 → 출퇴근 모두 보정 + tamMapper.afterProcessAbsence(p); + break; + case "0005": // 연차 → WORK_CD 변경 + case "0006": // 근무변경 → WORK_CD 변경 + tamMapper.afterProcessWorkChange(p); + break; + case "0008": // 외출 → OUTING_MIN/CNT 업데이트 + tamMapper.updateOutingTime(p); + break; + default: + break; + } + } + + // ────────────────────────────────────────────── + // TAM0040 - 근태 현황 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getTamStatusList(String corpNo, String yyvctYy, + String teamCd, String staYmd, + String endYmd, String includeRetireYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("yyvctYy", yyvctYy); + p.put("teamCd", teamCd); + p.put("staYmd", staYmd); + p.put("endYmd", endYmd); + p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N"); + return tamMapper.getTamStatusList(p); + } +} diff --git a/backend/src/main/java/com/company/gw/wplan/controller/WplanController.java b/backend/src/main/java/com/company/gw/wplan/controller/WplanController.java new file mode 100644 index 0000000..9eb92dc --- /dev/null +++ b/backend/src/main/java/com/company/gw/wplan/controller/WplanController.java @@ -0,0 +1,81 @@ +package com.company.gw.wplan.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.wplan.service.WplanService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/wplan") +@RequiredArgsConstructor +public class WplanController { + + private final WplanService wplanService; + + // ────────────────────────────────────────────── + // 근무코드 목록 + // ────────────────────────────────────────────── + + @GetMapping("/workcode") + public ApiResponse>> getWorkCodeList( + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(wplanService.getWorkCodeList(currentUser.getCorpNo())); + } + + // ────────────────────────────────────────────── + // Wplan0010 - 근무계획관리 + // ────────────────────────────────────────────── + + @GetMapping("/0010") + public ApiResponse>> getWplanList( + @RequestParam String workPlanYymm, + @RequestParam(defaultValue = "") String searchText, + @RequestParam(defaultValue = "") String teamCd, + @RequestParam(defaultValue = "N") String includeRetireYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(wplanService.getWplanList( + currentUser.getCorpNo(), workPlanYymm, searchText, teamCd, includeRetireYn)); + } + + @PostMapping("/0010") + public ApiResponse saveWplanList( + @RequestBody Map body, + @AuthenticationPrincipal CurrentUser currentUser) { + @SuppressWarnings("unchecked") + List> saveList = (List>) body.get("saveList"); + wplanService.saveWplanList(currentUser.getCorpNo(), saveList); + return ApiResponse.ok(null); + } + + // ────────────────────────────────────────────── + // Wplan0020 - 나의 근무계획 + // ────────────────────────────────────────────── + + @GetMapping("/0020") + public ApiResponse>> getMyWplanList( + @RequestParam String workPlanYymm, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(wplanService.getMyWplanList(currentUser.getCorpNo(), workPlanYymm)); + } + + // ────────────────────────────────────────────── + // Wplan0030 - 전체 근무계획 + // ────────────────────────────────────────────── + + @GetMapping("/0030") + public ApiResponse>> getAllWplanList( + @RequestParam String workPlanYymm, + @RequestParam(defaultValue = "") String teamCd, + @RequestParam(defaultValue = "") String dutyCd, + @RequestParam(defaultValue = "") String workCd, + @RequestParam(defaultValue = "WORK_CD") String workCdType, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(wplanService.getAllWplanList( + currentUser.getCorpNo(), workPlanYymm, teamCd, dutyCd, workCd, workCdType)); + } +} diff --git a/backend/src/main/java/com/company/gw/wplan/mapper/WplanMapper.java b/backend/src/main/java/com/company/gw/wplan/mapper/WplanMapper.java new file mode 100644 index 0000000..7ef1c88 --- /dev/null +++ b/backend/src/main/java/com/company/gw/wplan/mapper/WplanMapper.java @@ -0,0 +1,27 @@ +package com.company.gw.wplan.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface WplanMapper { + + // 근무코드 목록 + List> getWorkCodeList(Map param); + + // Wplan0010 - 근무계획관리 + List> getWplanList(Map param); + + // Wplan0020 - 나의 근무계획 + List> getMyWplanList(Map param); + + // Wplan0030 - 전체 근무계획 + List> getAllWplanList(Map param); + + // 저장/삭제 + void upsertWorkplan(Map param); + void deleteWorkplan(Map param); + void deleteWorkplanMonth(Map param); +} diff --git a/backend/src/main/java/com/company/gw/wplan/service/WplanService.java b/backend/src/main/java/com/company/gw/wplan/service/WplanService.java new file mode 100644 index 0000000..891a89e --- /dev/null +++ b/backend/src/main/java/com/company/gw/wplan/service/WplanService.java @@ -0,0 +1,97 @@ +package com.company.gw.wplan.service; + +import com.company.gw.common.util.SecurityUtil; +import com.company.gw.wplan.mapper.WplanMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class WplanService { + + private final WplanMapper wplanMapper; + + // ────────────────────────────────────────────── + // 근무코드 목록 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getWorkCodeList(String corpNo) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + return wplanMapper.getWorkCodeList(p); + } + + // ────────────────────────────────────────────── + // Wplan0010 - 근무계획관리 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getWplanList(String corpNo, String workPlanYymm, + String searchText, String teamCd, + String includeRetireYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("workPlanYymm", workPlanYymm); + p.put("searchText", searchText); + p.put("teamCd", teamCd); + p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N"); + return wplanMapper.getWplanList(p); + } + + /** + * 근무계획 일괄 저장 + * saveList: [{usrId, workPlanYymmdd, planWorkCd, sortOdr}] + */ + @Transactional + public void saveWplanList(String corpNo, List> saveList) { + String loginUsrId = SecurityUtil.getUsrId(); + for (Map item : saveList) { + Map p = new HashMap<>(item); + p.put("corpNo", corpNo); + p.put("loginUsrId", loginUsrId); + String planWorkCd = (String) p.get("planWorkCd"); + if (planWorkCd == null || planWorkCd.isEmpty()) { + wplanMapper.deleteWorkplan(p); + } else { + wplanMapper.upsertWorkplan(p); + } + } + } + + // ────────────────────────────────────────────── + // Wplan0020 - 나의 근무계획 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getMyWplanList(String corpNo, String workPlanYymm) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("usrId", SecurityUtil.getUsrId()); + p.put("workPlanYymm", workPlanYymm); + return wplanMapper.getMyWplanList(p); + } + + // ────────────────────────────────────────────── + // Wplan0030 - 전체 근무계획 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getAllWplanList(String corpNo, String workPlanYymm, + String teamCd, String dutyCd, + String workCd, String workCdType) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("workPlanYymm", workPlanYymm); + p.put("teamCd", teamCd); + p.put("dutyCd", dutyCd); + p.put("workCd", workCd); + p.put("workCdType", workCdType != null ? workCdType : "WORK_CD"); + return wplanMapper.getAllWplanList(p); + } +} diff --git a/backend/src/main/java/com/company/gw/wtime/controller/WtimeController.java b/backend/src/main/java/com/company/gw/wtime/controller/WtimeController.java new file mode 100644 index 0000000..f50829c --- /dev/null +++ b/backend/src/main/java/com/company/gw/wtime/controller/WtimeController.java @@ -0,0 +1,58 @@ +package com.company.gw.wtime.controller; + +import com.company.gw.common.dto.ApiResponse; +import com.company.gw.common.dto.CurrentUser; +import com.company.gw.wtime.service.WtimeService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/wtime") +@RequiredArgsConstructor +public class WtimeController { + + private final WtimeService wtimeService; + + // ────────────────────────────────────────────── + // Wtime0010 - 개인별 근무시간 + // ────────────────────────────────────────────── + + @GetMapping("/0010") + public ApiResponse>> getWtimeList( + @RequestParam String staYmd, + @RequestParam String endYmd, + @RequestParam(defaultValue = "") String usrId, + @RequestParam(defaultValue = "") String usrNm, + @RequestParam(defaultValue = "") String teamCd, + @RequestParam(defaultValue = "") String dutyCd, + @RequestParam(defaultValue = "N") String includeRetireYn, + @RequestParam(defaultValue = "") String includeWorkYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(wtimeService.getWtimeList( + currentUser.getCorpNo(), staYmd, endYmd, + usrId, usrNm, teamCd, dutyCd, includeRetireYn, includeWorkYn)); + } + + // ────────────────────────────────────────────── + // Wtime0030 - 월별 근무시간 집계 + // ────────────────────────────────────────────── + + @GetMapping("/0030") + public ApiResponse>> getWstatList( + @RequestParam String staYmd, + @RequestParam String endYmd, + @RequestParam(defaultValue = "") String usrId, + @RequestParam(defaultValue = "") String usrNm, + @RequestParam(defaultValue = "") String teamCd, + @RequestParam(defaultValue = "") String dutyCd, + @RequestParam(defaultValue = "N") String includeRetireYn, + @AuthenticationPrincipal CurrentUser currentUser) { + return ApiResponse.ok(wtimeService.getWstatList( + currentUser.getCorpNo(), staYmd, endYmd, + usrId, usrNm, teamCd, dutyCd, includeRetireYn)); + } +} diff --git a/backend/src/main/java/com/company/gw/wtime/mapper/WtimeMapper.java b/backend/src/main/java/com/company/gw/wtime/mapper/WtimeMapper.java new file mode 100644 index 0000000..a57a347 --- /dev/null +++ b/backend/src/main/java/com/company/gw/wtime/mapper/WtimeMapper.java @@ -0,0 +1,16 @@ +package com.company.gw.wtime.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface WtimeMapper { + + // Wtime0010 - 개인별 근무시간 + List> getWtimeList(Map param); + + // Wtime0030 - 월별 근무시간 집계 + List> getWstatList(Map param); +} diff --git a/backend/src/main/java/com/company/gw/wtime/service/WtimeService.java b/backend/src/main/java/com/company/gw/wtime/service/WtimeService.java new file mode 100644 index 0000000..5f15f28 --- /dev/null +++ b/backend/src/main/java/com/company/gw/wtime/service/WtimeService.java @@ -0,0 +1,59 @@ +package com.company.gw.wtime.service; + +import com.company.gw.wtime.mapper.WtimeMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class WtimeService { + + private final WtimeMapper wtimeMapper; + + // ────────────────────────────────────────────── + // Wtime0010 - 개인별 근무시간 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getWtimeList(String corpNo, String staYmd, String endYmd, + String usrId, String usrNm, String teamCd, + String dutyCd, String includeRetireYn, + String includeWorkYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("staYmd", staYmd); + p.put("endYmd", endYmd); + p.put("usrId", usrId); + p.put("usrNm", usrNm); + p.put("teamCd", teamCd); + p.put("dutyCd", dutyCd); + p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N"); + p.put("includeWorkYn", includeWorkYn); + return wtimeMapper.getWtimeList(p); + } + + // ────────────────────────────────────────────── + // Wtime0030 - 월별 근무시간 집계 + // ────────────────────────────────────────────── + + @Transactional(readOnly = true) + public List> getWstatList(String corpNo, String staYmd, String endYmd, + String usrId, String usrNm, String teamCd, + String dutyCd, String includeRetireYn) { + Map p = new HashMap<>(); + p.put("corpNo", corpNo); + p.put("staYmd", staYmd); + p.put("endYmd", endYmd); + p.put("usrId", usrId); + p.put("usrNm", usrNm); + p.put("teamCd", teamCd); + p.put("dutyCd", dutyCd); + p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N"); + return wtimeMapper.getWstatList(p); + } +} diff --git a/backend/src/main/resources/application-mariadb.yml b/backend/src/main/resources/application-mariadb.yml new file mode 100644 index 0000000..9ce6d94 --- /dev/null +++ b/backend/src/main/resources/application-mariadb.yml @@ -0,0 +1,17 @@ +# MariaDB 설정 (추후 전환용) +spring: + datasource: + url: ${DB_URL:jdbc:mariadb://localhost:3306/gw} + username: ${DB_USERNAME:} + password: ${DB_PASSWORD:} + driver-class-name: org.mariadb.jdbc.Driver + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + +mybatis: + configuration: + database-id: mariadb diff --git a/backend/src/main/resources/application-mssql.yml b/backend/src/main/resources/application-mssql.yml new file mode 100644 index 0000000..ae61713 --- /dev/null +++ b/backend/src/main/resources/application-mssql.yml @@ -0,0 +1,13 @@ +# MS SQL Server 설정 (현재 운영) +spring: + datasource: + url: ${DB_URL:jdbc:sqlserver://121.156.116.136:52785;databaseName=logins_test;encrypt=false;trustServerCertificate=true;sendStringParametersAsUnicode=false} + username: ${DB_USERNAME:logins} + password: ${DB_PASSWORD:ghkfkdahrfh40-8} + driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 diff --git a/backend/src/main/resources/application-oracle.yml b/backend/src/main/resources/application-oracle.yml new file mode 100644 index 0000000..b1f3db7 --- /dev/null +++ b/backend/src/main/resources/application-oracle.yml @@ -0,0 +1,17 @@ +# Oracle DB 설정 (현재 운영) +spring: + datasource: + url: ${DB_URL:jdbc:oracle:thin:@localhost:1521:ORCL} + username: ${DB_USERNAME:} + password: ${DB_PASSWORD:} + driver-class-name: oracle.jdbc.OracleDriver + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + +mybatis: + configuration: + database-id: oracle diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..4ab3de2 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,62 @@ +spring: + application: + name: gw-backend + + # Active Profile: mssql | mariadb + profiles: + active: mssql + + # Redis + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + + # RabbitMQ + rabbitmq: + host: ${RABBITMQ_HOST:localhost} + port: ${RABBITMQ_PORT:5672} + username: ${RABBITMQ_USER:guest} + password: ${RABBITMQ_PASSWORD:guest} + + # File Upload + servlet: + multipart: + max-file-size: 50MB + max-request-size: 100MB + +# MyBatis +mybatis: + mapper-locations: classpath:mapper/**/*.xml + configuration: + map-underscore-to-camel-case: true + default-fetch-size: 100 + default-statement-timeout: 30 + +# JWT +jwt: + secret: ${JWT_SECRET:your-secret-key-must-be-at-least-256-bits-long-for-hs256} + access-token-expiry: 1800000 # 30분 (ms) + refresh-token-expiry: 604800000 # 7일 (ms) + +# File Storage +file: + upload-path: ${FILE_UPLOAD_PATH:/data/uploads} + +# Actuator +management: + endpoints: + web: + exposure: + include: health,info + base-path: /actuator + endpoint: + health: + show-details: when-authorized + +# Logging +logging: + level: + com.company.gw: DEBUG + org.mybatis: DEBUG diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b6272bb --- /dev/null +++ b/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + ${LOG_PATTERN} + UTF-8 + + + + + + /var/log/${APP_NAME}/app.log + + + /var/log/${APP_NAME}/app.%d{yyyy-MM-dd}.%i.log.gz + + 100MB + + 30 + 3GB + + + ${LOG_PATTERN} + UTF-8 + + + + + + /var/log/${APP_NAME}/error.log + + ERROR + + + /var/log/${APP_NAME}/error.%d{yyyy-MM-dd}.log.gz + 60 + + + ${LOG_PATTERN} + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/board/board_sql.xml b/backend/src/main/resources/mapper/board/board_sql.xml new file mode 100644 index 0000000..5f8978f --- /dev/null +++ b/backend/src/main/resources/mapper/board/board_sql.xml @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO SX_GW0020 ( + CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO, + BBS_TITLE_NM, BBS_CN, INQR_CNT, + CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{untyBbsCd}, #{untyBbsSno}, + #{bbsTitleNm}, #{bbsCn}, 0, + #{usrId}, #{usrId}, GETDATE(), #{usrId}, GETDATE() + ) + + + + + UPDATE SX_GW0020 + SET BBS_TITLE_NM = #{bbsTitleNm}, + BBS_CN = #{bbsCn}, + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + + UPDATE SX_GW0020 + SET INQR_CNT = ISNULL(INQR_CNT, 0) + 1 + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + + UPDATE SX_GW0020 + SET ETC_ATCH_NO = #{etcAtchNo}, + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + + UPDATE SX_GW0020 + SET PHOTO_ATCH_NO = #{photoAtchNo}, + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + + UPDATE SX_GW0020 + SET CMMT_CNT = ( + SELECT COUNT(1) FROM SX_GW0030 + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + ), + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + + DELETE FROM SX_GW0020 + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + + + + + + + + INSERT INTO SX_GW0030 ( + CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO, CMMT_SNO, + CMMT_CN, CMMT_CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{untyBbsCd}, #{untyBbsSno}, #{cmmtSno}, + #{cmmtCn}, #{usrId}, #{usrId}, GETDATE(), #{usrId}, GETDATE() + ) + + + + + DELETE FROM SX_GW0030 + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + AND CMMT_SNO = #{cmmtSno} + + + + + DELETE FROM SX_GW0030 + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + + + + + + + + + INSERT INTO SX_GW0020 ( + CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO, + BBS_TITLE_NM, BBS_CN, INQR_CNT, + CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{untyBbsCd}, #{untyBbsSno}, + #{bbsTitleNm}, #{bbsCn}, 0, + #{usrId}, #{usrId}, NOW(), #{usrId}, NOW() + ) + + + + UPDATE SX_GW0020 + SET BBS_TITLE_NM = #{bbsTitleNm}, + BBS_CN = #{bbsCn}, + MODID = #{usrId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + UPDATE SX_GW0020 + SET INQR_CNT = IFNULL(INQR_CNT, 0) + 1 + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + UPDATE SX_GW0020 + SET ETC_ATCH_NO = #{etcAtchNo}, + MODID = #{usrId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + UPDATE SX_GW0020 + SET PHOTO_ATCH_NO = #{photoAtchNo}, + MODID = #{usrId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + UPDATE SX_GW0020 + SET CMMT_CNT = ( + SELECT COUNT(1) FROM SX_GW0030 + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + ), + MODID = #{usrId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND UNTY_BBS_CD = #{untyBbsCd} + AND UNTY_BBS_SNO = #{untyBbsSno} + + + + INSERT INTO SX_GW0030 ( + CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO, CMMT_SNO, + CMMT_CN, CMMT_CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{untyBbsCd}, #{untyBbsSno}, #{cmmtSno}, + #{cmmtCn}, #{usrId}, #{usrId}, NOW(), #{usrId}, NOW() + ) + + + + + diff --git a/backend/src/main/resources/mapper/common/attach_sql.xml b/backend/src/main/resources/mapper/common/attach_sql.xml new file mode 100644 index 0000000..0b3cff5 --- /dev/null +++ b/backend/src/main/resources/mapper/common/attach_sql.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + INSERT INTO SX_CO0050 ( + ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM, + ATCH_TYPE_NM, ATCH_FILE_MG, DEL_DT, TBL_COL_NM, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{atchfileNo}, #{atchNo}, #{atchFilePathNm}, #{atchFileNm}, + #{atchTypeNm}, #{atchFileMg}, NULL, #{tblColNm}, + #{rgstrId}, GETDATE(), #{rgstrId}, GETDATE() + ) + + + + DELETE FROM SX_CO0050 WHERE ATCHFILE_NO = #{atchfileNo} + + + + UPDATE SX_CO0050 + SET TBL_COL_NM = #{tblColNm} + WHERE ATCH_NO = #{atchNo} + + + + UPDATE SX_CO0050 + SET TBL_COL_NM = #{tblColNm} + WHERE ATCHFILE_NO = #{atchfileNo} + + + + + + + + + + + + INSERT INTO SX_CO0050 ( + ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM, + ATCH_TYPE_NM, ATCH_FILE_MG, DEL_DT, TBL_COL_NM, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{atchfileNo}, #{atchNo}, #{atchFilePathNm}, #{atchFileNm}, + #{atchTypeNm}, #{atchFileMg}, NULL, #{tblColNm}, + #{rgstrId}, NOW(), #{rgstrId}, NOW() + ) + + + + DELETE FROM SX_CO0050 WHERE ATCHFILE_NO = #{atchfileNo} + + + + UPDATE SX_CO0050 + SET TBL_COL_NM = #{tblColNm} + WHERE ATCH_NO = #{atchNo} + + + + UPDATE SX_CO0050 + SET TBL_COL_NM = #{tblColNm} + WHERE ATCHFILE_NO = #{atchfileNo} + + + + + diff --git a/backend/src/main/resources/mapper/common/code_sql.xml b/backend/src/main/resources/mapper/common/code_sql.xml new file mode 100644 index 0000000..4456ef9 --- /dev/null +++ b/backend/src/main/resources/mapper/common/code_sql.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/common/common_sql.xml b/backend/src/main/resources/mapper/common/common_sql.xml new file mode 100644 index 0000000..d3aba1f --- /dev/null +++ b/backend/src/main/resources/mapper/common/common_sql.xml @@ -0,0 +1,28 @@ + + + + + + + + MERGE INTO SX_CO0060 T + USING (SELECT 1 AS DUMMY) S + ON (T.SEQUENCE_NM = #{sequenceNm}) + WHEN NOT MATCHED THEN + INSERT (SEQUENCE_NM, SEQUENCE_VAL) VALUES (#{sequenceNm}, 1) + WHEN MATCHED THEN + UPDATE SET SEQUENCE_VAL = SEQUENCE_VAL + 1; + + + + INSERT INTO SX_CO0060 (SEQUENCE_NM, SEQUENCE_VAL) + VALUES (#{sequenceNm}, 1) + ON DUPLICATE KEY UPDATE SEQUENCE_VAL = SEQUENCE_VAL + 1 + + + + + diff --git a/backend/src/main/resources/mapper/common/menu_sql.xml b/backend/src/main/resources/mapper/common/menu_sql.xml new file mode 100644 index 0000000..093e7b1 --- /dev/null +++ b/backend/src/main/resources/mapper/common/menu_sql.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/common/security_sql.xml b/backend/src/main/resources/mapper/common/security_sql.xml new file mode 100644 index 0000000..e69b8d2 --- /dev/null +++ b/backend/src/main/resources/mapper/common/security_sql.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/backend/src/main/resources/mapper/common/user_sql.xml b/backend/src/main/resources/mapper/common/user_sql.xml new file mode 100644 index 0000000..db97292 --- /dev/null +++ b/backend/src/main/resources/mapper/common/user_sql.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + UPDATE SX_GW0010 + SET PW = #{newPw} + WHERE USR_ID = #{usrId} + + + diff --git a/backend/src/main/resources/mapper/envset/envset0010_sql.xml b/backend/src/main/resources/mapper/envset/envset0010_sql.xml new file mode 100644 index 0000000..26dbeda --- /dev/null +++ b/backend/src/main/resources/mapper/envset/envset0010_sql.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + INSERT INTO SX_GW0010 ( + CORP_NO, USR_ID, USR_SORT_ORDR, USR_NM, PW, + DUTY_CD, BRTHDY_DATE, USR_TELNO, MTEL_NO, EMAIL, + BASE_ADRS, GUSO_ADRS, GUSO_TELNO, JOINCP_DATE, TEAM_CD, + DISML_DATE, RETIREMENT_DATE, SPCLT_ARTC_CN, + RGSTR_ID, RGST_DT, MODID, UPD_DT, + LOGIN_ID, SPMT_EMAIL, SPMT_MTEL_NO, RRNO, + APPR_YN, FINAL_SCHSP_NM + ) VALUES ( + #{corpNo}, #{usrId}, #{usrSortOrdr}, #{usrNm}, #{pw}, + #{dutyCd}, #{brthDyDate}, #{usrTelno}, #{mtelNo}, #{email}, + #{baseAdrs}, #{gusoAdrs}, #{gusoTelno}, #{joinCpDate}, #{teamCd}, + #{dismlDate}, #{retirementDate}, #{spkltArtcCn}, + #{rgstrId}, GETDATE(), #{rgstrId}, GETDATE(), + #{loginId}, #{spmtEmail}, #{spmtMtelNo}, #{rrno}, + ISNULL(NULLIF(#{apprYn}, ''), 'N'), #{finalSchspNm} + ) + + + + INSERT INTO SX_GW0010 ( + CORP_NO, USR_ID, USR_SORT_ORDR, USR_NM, PW, + DUTY_CD, BRTHDY_DATE, USR_TELNO, MTEL_NO, EMAIL, + BASE_ADRS, GUSO_ADRS, GUSO_TELNO, JOINCP_DATE, TEAM_CD, + DISML_DATE, RETIREMENT_DATE, SPCLT_ARTC_CN, + RGSTR_ID, RGST_DT, MODID, UPD_DT, + LOGIN_ID, SPMT_EMAIL, SPMT_MTEL_NO, RRNO, + APPR_YN, FINAL_SCHSP_NM + ) VALUES ( + #{corpNo}, #{usrId}, #{usrSortOrdr}, #{usrNm}, #{pw}, + #{dutyCd}, #{brthDyDate}, #{usrTelno}, #{mtelNo}, #{email}, + #{baseAdrs}, #{gusoAdrs}, #{gusoTelno}, #{joinCpDate}, #{teamCd}, + #{dismlDate}, #{retirementDate}, #{spkltArtcCn}, + #{rgstrId}, NOW(), #{rgstrId}, NOW(), + #{loginId}, #{spmtEmail}, #{spmtMtelNo}, #{rrno}, + IFNULL(NULLIF(#{apprYn}, ''), 'N'), #{finalSchspNm} + ) + + + + + UPDATE SX_GW0010 + SET USR_SORT_ORDR = #{usrSortOrdr}, + USR_NM = #{usrNm}, + DUTY_CD = #{dutyCd}, + BRTHDY_DATE = #{brthDyDate}, + USR_TELNO = #{usrTelno}, + MTEL_NO = #{mtelNo}, + EMAIL = #{email}, + BASE_ADRS = #{baseAdrs}, + GUSO_ADRS = #{gusoAdrs}, + GUSO_TELNO = #{gusoTelno}, + JOINCP_DATE = #{joinCpDate}, + TEAM_CD = #{teamCd}, + DISML_DATE = #{dismlDate}, + RETIREMENT_DATE = #{retirementDate}, + SPCLT_ARTC_CN = #{spkltArtcCn}, + LOGIN_ID = #{loginId}, + SPMT_EMAIL = #{spmtEmail}, + SPMT_MTEL_NO = #{spmtMtelNo}, + RRNO = #{rrno}, + APPR_YN = ISNULL(NULLIF(#{apprYn}, ''), 'N'), + FINAL_SCHSP_NM = #{finalSchspNm}, + MODID = #{modId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + + + + UPDATE SX_GW0010 + SET USR_SORT_ORDR = #{usrSortOrdr}, + USR_NM = #{usrNm}, + DUTY_CD = #{dutyCd}, + BRTHDY_DATE = #{brthDyDate}, + USR_TELNO = #{usrTelno}, + MTEL_NO = #{mtelNo}, + EMAIL = #{email}, + BASE_ADRS = #{baseAdrs}, + GUSO_ADRS = #{gusoAdrs}, + GUSO_TELNO = #{gusoTelno}, + JOINCP_DATE = #{joinCpDate}, + TEAM_CD = #{teamCd}, + DISML_DATE = #{dismlDate}, + RETIREMENT_DATE = #{retirementDate}, + SPCLT_ARTC_CN = #{spkltArtcCn}, + LOGIN_ID = #{loginId}, + SPMT_EMAIL = #{spmtEmail}, + SPMT_MTEL_NO = #{spmtMtelNo}, + RRNO = #{rrno}, + APPR_YN = IFNULL(NULLIF(#{apprYn}, ''), 'N'), + FINAL_SCHSP_NM = #{finalSchspNm}, + MODID = #{modId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + + + + + UPDATE SX_GW0010 + SET PW = #{pw}, + MODID = #{modId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + + + + UPDATE SX_GW0010 + SET PW = #{pw}, + MODID = #{modId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + + + + + DELETE FROM SX_GW0010 + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + + + + + UPDATE SX_GW0010 + SET PHOTO_ATCHFILE_NO = #{photoAtchfileNo}, + MODID = #{modId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + + + + UPDATE SX_GW0010 + SET PHOTO_ATCHFILE_NO = #{photoAtchfileNo}, + MODID = #{modId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + + + diff --git a/backend/src/main/resources/mapper/envset/envset0020_sql.xml b/backend/src/main/resources/mapper/envset/envset0020_sql.xml new file mode 100644 index 0000000..181eb4e --- /dev/null +++ b/backend/src/main/resources/mapper/envset/envset0020_sql.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + INSERT INTO SX_CO0030 ( + CORP_NO, COMM_CL_CD, COMM_CL_CD_NM, COMM_CL_CD_USE_YN, + COMM_CL_CD_DSCRPT, DSPLY_ORDR, PROP_YN, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{commClCd}, #{commClCdNm}, #{commClCdUseYn}, + #{commClCdDscrpt}, #{dsplyOrdr}, 'N', + #{rgstrId}, GETDATE(), #{rgstrId}, GETDATE() + ) + + + + INSERT INTO SX_CO0030 ( + CORP_NO, COMM_CL_CD, COMM_CL_CD_NM, COMM_CL_CD_USE_YN, + COMM_CL_CD_DSCRPT, DSPLY_ORDR, PROP_YN, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{commClCd}, #{commClCdNm}, #{commClCdUseYn}, + #{commClCdDscrpt}, #{dsplyOrdr}, 'N', + #{rgstrId}, NOW(), #{rgstrId}, NOW() + ) + + + + UPDATE SX_CO0030 + SET COMM_CL_CD_NM = #{commClCdNm}, + COMM_CL_CD_USE_YN = #{commClCdUseYn}, + COMM_CL_CD_DSCRPT = #{commClCdDscrpt}, + DSPLY_ORDR = #{dsplyOrdr}, + MODID = #{modId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND COMM_CL_CD = #{commClCd} + + + + UPDATE SX_CO0030 + SET COMM_CL_CD_NM = #{commClCdNm}, + COMM_CL_CD_USE_YN = #{commClCdUseYn}, + COMM_CL_CD_DSCRPT = #{commClCdDscrpt}, + DSPLY_ORDR = #{dsplyOrdr}, + MODID = #{modId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND COMM_CL_CD = #{commClCd} + + + + DELETE FROM SX_CO0030 + WHERE CORP_NO = #{corpNo} + AND COMM_CL_CD = #{commClCd} + + + + + + + + INSERT INTO SX_CO0040 ( + CORP_NO, COMM_CL_CD, COMM_CD, COMM_CD_NM, + COMM_CD_USE_YN, COMM_CD_DSCRPT, COMM_CD_DSPLY_ORDR, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{commClCd}, #{commCd}, #{commCdNm}, + #{commCdUseYn}, #{commCdDscrpt}, #{commCdDsplyOrdr}, + #{rgstrId}, GETDATE(), #{rgstrId}, GETDATE() + ) + + + + INSERT INTO SX_CO0040 ( + CORP_NO, COMM_CL_CD, COMM_CD, COMM_CD_NM, + COMM_CD_USE_YN, COMM_CD_DSCRPT, COMM_CD_DSPLY_ORDR, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{commClCd}, #{commCd}, #{commCdNm}, + #{commCdUseYn}, #{commCdDscrpt}, #{commCdDsplyOrdr}, + #{rgstrId}, NOW(), #{rgstrId}, NOW() + ) + + + + UPDATE SX_CO0040 + SET COMM_CD_NM = #{commCdNm}, + COMM_CD_USE_YN = #{commCdUseYn}, + COMM_CD_DSCRPT = #{commCdDscrpt}, + COMM_CD_DSPLY_ORDR = #{commCdDsplyOrdr}, + MODID = #{modId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND COMM_CL_CD = #{commClCd} + AND COMM_CD = #{commCd} + + + + UPDATE SX_CO0040 + SET COMM_CD_NM = #{commCdNm}, + COMM_CD_USE_YN = #{commCdUseYn}, + COMM_CD_DSCRPT = #{commCdDscrpt}, + COMM_CD_DSPLY_ORDR = #{commCdDsplyOrdr}, + MODID = #{modId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND COMM_CL_CD = #{commClCd} + AND COMM_CD = #{commCd} + + + + DELETE FROM SX_CO0040 + WHERE CORP_NO = #{corpNo} + AND COMM_CL_CD = #{commClCd} + AND COMM_CD = #{commCd} + + + diff --git a/backend/src/main/resources/mapper/envset/envset0040_sql.xml b/backend/src/main/resources/mapper/envset/envset0040_sql.xml new file mode 100644 index 0000000..f7e1e53 --- /dev/null +++ b/backend/src/main/resources/mapper/envset/envset0040_sql.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + INSERT INTO SX_CO0070 ( + CORP_NO, WORK_CD, WORK_CD_TITLE_NM, GOTOWORK_TM_NM, GETOFFWORK_TM_NM, + PAY_DIV_NM, REGULAR_WORK_TM, REST_TM, + INCLU_WORK_NM, INCLU_WORK_START_TM, INCLU_WORK_END_TM, INCLU_WORK_TM, + CALC_LOCA_NM, OT_START_TM, NGT_ALLW_NM, NGT_START_TM, NGT_END_TM, + WORK_ALLW_NM, WORK_CD_USE_YN, WORK_CN, + OT_BSCRT, NGT_BSCRT, APLY_WORK_DAY, YYCT_DEDU_DAY, + MEAL_START_TM, MEAL_END_TM, HOLIDAY_YN, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{workCd}, #{workCdTitleNm}, #{gotoworkTmNm}, #{getoffworkTmNm}, + #{payDivNm}, #{regularWorkTm}, #{restTm}, + #{incluWorkNm}, #{incluWorkStartTm}, #{incluWorkEndTm}, #{incluWorkTm}, + #{calcLocaNm}, #{otStartTm}, #{ngtAllwNm}, #{ngtStartTm}, #{ngtEndTm}, + #{workAllwNm}, #{workCdUseYn}, #{workCn}, + #{otBscrt}, #{ngtBscrt}, #{aplyWorkDay}, #{yyctDeduDay}, + #{mealStartTm}, #{mealEndTm}, #{holidayYn}, + #{regUsrId}, GETDATE(), #{regUsrId}, GETDATE() + ) + + + + + INSERT INTO SX_CO0070 ( + CORP_NO, WORK_CD, WORK_CD_TITLE_NM, GOTOWORK_TM_NM, GETOFFWORK_TM_NM, + PAY_DIV_NM, REGULAR_WORK_TM, REST_TM, + INCLU_WORK_NM, INCLU_WORK_START_TM, INCLU_WORK_END_TM, INCLU_WORK_TM, + CALC_LOCA_NM, OT_START_TM, NGT_ALLW_NM, NGT_START_TM, NGT_END_TM, + WORK_ALLW_NM, WORK_CD_USE_YN, WORK_CN, + OT_BSCRT, NGT_BSCRT, APLY_WORK_DAY, YYCT_DEDU_DAY, + MEAL_START_TM, MEAL_END_TM, HOLIDAY_YN, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{workCd}, #{workCdTitleNm}, #{gotoworkTmNm}, #{getoffworkTmNm}, + #{payDivNm}, #{regularWorkTm}, #{restTm}, + #{incluWorkNm}, #{incluWorkStartTm}, #{incluWorkEndTm}, #{incluWorkTm}, + #{calcLocaNm}, #{otStartTm}, #{ngtAllwNm}, #{ngtStartTm}, #{ngtEndTm}, + #{workAllwNm}, #{workCdUseYn}, #{workCn}, + #{otBscrt}, #{ngtBscrt}, #{aplyWorkDay}, #{yyctDeduDay}, + #{mealStartTm}, #{mealEndTm}, #{holidayYn}, + #{regUsrId}, NOW(), #{regUsrId}, NOW() + ) + + + + + UPDATE SX_CO0070 SET + WORK_CD_TITLE_NM = #{workCdTitleNm}, + GOTOWORK_TM_NM = #{gotoworkTmNm}, + GETOFFWORK_TM_NM = #{getoffworkTmNm}, + PAY_DIV_NM = #{payDivNm}, + REGULAR_WORK_TM = #{regularWorkTm}, + REST_TM = #{restTm}, + INCLU_WORK_NM = #{incluWorkNm}, + INCLU_WORK_START_TM = #{incluWorkStartTm}, + INCLU_WORK_END_TM = #{incluWorkEndTm}, + INCLU_WORK_TM = #{incluWorkTm}, + CALC_LOCA_NM = #{calcLocaNm}, + OT_START_TM = #{otStartTm}, + NGT_ALLW_NM = #{ngtAllwNm}, + NGT_START_TM = #{ngtStartTm}, + NGT_END_TM = #{ngtEndTm}, + WORK_ALLW_NM = #{workAllwNm}, + WORK_CD_USE_YN = #{workCdUseYn}, + WORK_CN = #{workCn}, + OT_BSCRT = #{otBscrt}, + NGT_BSCRT = #{ngtBscrt}, + APLY_WORK_DAY = #{aplyWorkDay}, + YYCT_DEDU_DAY = #{yyctDeduDay}, + MEAL_START_TM = #{mealStartTm}, + MEAL_END_TM = #{mealEndTm}, + HOLIDAY_YN = #{holidayYn}, + MODID = #{regUsrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND WORK_CD = #{workCd} + + + + + UPDATE SX_CO0070 SET + WORK_CD_TITLE_NM = #{workCdTitleNm}, + GOTOWORK_TM_NM = #{gotoworkTmNm}, + GETOFFWORK_TM_NM = #{getoffworkTmNm}, + PAY_DIV_NM = #{payDivNm}, + REGULAR_WORK_TM = #{regularWorkTm}, + REST_TM = #{restTm}, + INCLU_WORK_NM = #{incluWorkNm}, + INCLU_WORK_START_TM = #{incluWorkStartTm}, + INCLU_WORK_END_TM = #{incluWorkEndTm}, + INCLU_WORK_TM = #{incluWorkTm}, + CALC_LOCA_NM = #{calcLocaNm}, + OT_START_TM = #{otStartTm}, + NGT_ALLW_NM = #{ngtAllwNm}, + NGT_START_TM = #{ngtStartTm}, + NGT_END_TM = #{ngtEndTm}, + WORK_ALLW_NM = #{workAllwNm}, + WORK_CD_USE_YN = #{workCdUseYn}, + WORK_CN = #{workCn}, + OT_BSCRT = #{otBscrt}, + NGT_BSCRT = #{ngtBscrt}, + APLY_WORK_DAY = #{aplyWorkDay}, + YYCT_DEDU_DAY = #{yyctDeduDay}, + MEAL_START_TM = #{mealStartTm}, + MEAL_END_TM = #{mealEndTm}, + HOLIDAY_YN = #{holidayYn}, + MODID = #{regUsrId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND WORK_CD = #{workCd} + + + + + DELETE FROM SX_CO0070 + WHERE CORP_NO = #{corpNo} + AND WORK_CD = #{workCd} + + + diff --git a/backend/src/main/resources/mapper/envset/envset0050_sql.xml b/backend/src/main/resources/mapper/envset/envset0050_sql.xml new file mode 100644 index 0000000..f7bebcb --- /dev/null +++ b/backend/src/main/resources/mapper/envset/envset0050_sql.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + INSERT INTO SX_CO0080 ( + CORP_NO, MENU_NO, UPPER_MENU_NO, MENU_NM, URL, RM, + MENU_PROP, MENU_ORDR, MENU_USE_YN, + RGSTR_ID, RGST_DT, MODID, UPD_DT, CONTROLLER, MENU_LVL + ) VALUES ( + #{corpNo}, #{menuNo}, NULLIF(#{upperMenuNo}, ''), #{menuNm}, #{url}, #{rm}, + #{menuProp}, #{menuOrdr}, #{menuUseYn}, + #{rgstrId}, GETDATE(), #{rgstrId}, GETDATE(), #{controller}, #{menuLvl} + ) + + + + INSERT INTO SX_CO0080 ( + CORP_NO, MENU_NO, UPPER_MENU_NO, MENU_NM, URL, RM, + MENU_PROP, MENU_ORDR, MENU_USE_YN, + RGSTR_ID, RGST_DT, MODID, UPD_DT, CONTROLLER, MENU_LVL + ) VALUES ( + #{corpNo}, #{menuNo}, NULLIF(#{upperMenuNo}, ''), #{menuNm}, #{url}, #{rm}, + #{menuProp}, #{menuOrdr}, #{menuUseYn}, + #{rgstrId}, NOW(), #{rgstrId}, NOW(), #{controller}, #{menuLvl} + ) + + + + UPDATE SX_CO0080 + SET UPPER_MENU_NO = NULLIF(#{upperMenuNo}, ''), + MENU_NM = #{menuNm}, + URL = #{url}, + RM = #{rm}, + MENU_PROP = #{menuProp}, + MENU_ORDR = #{menuOrdr}, + MENU_USE_YN = #{menuUseYn}, + CONTROLLER = #{controller}, + MENU_LVL = #{menuLvl}, + MODID = #{modId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND MENU_NO = #{menuNo} + + + + UPDATE SX_CO0080 + SET UPPER_MENU_NO = NULLIF(#{upperMenuNo}, ''), + MENU_NM = #{menuNm}, + URL = #{url}, + RM = #{rm}, + MENU_PROP = #{menuProp}, + MENU_ORDR = #{menuOrdr}, + MENU_USE_YN = #{menuUseYn}, + CONTROLLER = #{controller}, + MENU_LVL = #{menuLvl}, + MODID = #{modId}, + UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND MENU_NO = #{menuNo} + + + + DELETE FROM SX_CO0090 WHERE CORP_NO = #{corpNo} AND MENU_NO = #{menuNo} + + + + DELETE FROM SX_CO0080 WHERE CORP_NO = #{corpNo} AND MENU_NO = #{menuNo} + + + + + + + + + + MERGE INTO SX_CO0090 T + USING (SELECT 1 X) S + ON (T.CORP_NO = #{corpNo} AND T.MENU_NO = #{menuNo} AND T.MENU_AUTH_CD = #{menuAuthCd}) + WHEN NOT MATCHED THEN + INSERT (CORP_NO, MENU_NO, MENU_AUTH_CD, FUNC_AUTH_CN, RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{menuNo}, #{menuAuthCd}, #{funcAuthCn}, #{rgstrId}, GETDATE(), #{rgstrId}, GETDATE()) + WHEN MATCHED THEN + UPDATE SET FUNC_AUTH_CN = #{funcAuthCn}, MODID = #{modId}, UPD_DT = GETDATE(); + + + + INSERT INTO SX_CO0090 (CORP_NO, MENU_NO, MENU_AUTH_CD, FUNC_AUTH_CN, RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{menuNo}, #{menuAuthCd}, #{funcAuthCn}, #{rgstrId}, NOW(), #{rgstrId}, NOW()) + ON DUPLICATE KEY UPDATE FUNC_AUTH_CN = #{funcAuthCn}, MODID = #{modId}, UPD_DT = NOW() + + + + DELETE FROM SX_CO0090 + WHERE CORP_NO = #{corpNo} AND MENU_NO = #{menuNo} AND MENU_AUTH_CD = #{menuAuthCd} + + + + + + + + INSERT INTO SX_GW0130 (CORP_NO, USR_ID, MENU_AUTH_CD, RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{usrId}, #{menuAuthCd}, #{rgstrId}, GETDATE(), #{rgstrId}, GETDATE()) + + + + INSERT INTO SX_GW0130 (CORP_NO, USR_ID, MENU_AUTH_CD, RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{usrId}, #{menuAuthCd}, #{rgstrId}, NOW(), #{rgstrId}, NOW()) + + + + DELETE FROM SX_GW0130 + WHERE CORP_NO = #{corpNo} AND USR_ID = #{usrId} AND MENU_AUTH_CD = #{menuAuthCd} + + + + + + + + diff --git a/backend/src/main/resources/mapper/fedex/fedex_sql.xml b/backend/src/main/resources/mapper/fedex/fedex_sql.xml new file mode 100644 index 0000000..f80da68 --- /dev/null +++ b/backend/src/main/resources/mapper/fedex/fedex_sql.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + INSERT INTO errorMng ( + ie_gbn, s_code, s_year, s_jechl, com_nm, j_singo_dt, s_singo_dt, + j_gwi_cd, reg_user_id, f_st_cd, f_jj_cd, f_jj_contents, + f_jj_nm, f_jj_reg_dt, f_jj_reg_gbn, ATTACH_NO + ) VALUES ( + #{ieGbn}, #{sCode}, #{sYear}, #{sJechl}, #{comNm}, #{jSingoDt}, #{sSingoDt}, + #{jGwiCd}, #{regUserId}, #{fStCd}, #{fJjCd}, #{fJjContents}, + #{fJjNm}, #{fJjRegDt}, #{fJjRegGbn}, + #{attachNo}'' + ) + + + + + UPDATE errorMng SET ATTACH_NO = #{attachNo} WHERE sq = #{sq} + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/main/main_sql.xml b/backend/src/main/resources/mapper/main/main_sql.xml new file mode 100644 index 0000000..ef6bc31 --- /dev/null +++ b/backend/src/main/resources/mapper/main/main_sql.xml @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE SX_GW0050 + SET WORK_START_DT = GETDATE(), + REAL_START_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + AND WORK_PLAN_YYMMDD = #{workPlanYymmdd} + + + + + UPDATE SX_GW0050 + SET WORK_START_DT = NOW(), + REAL_START_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + AND WORK_PLAN_YYMMDD = #{workPlanYymmdd} + + + + + UPDATE SX_GW0050 + SET WORK_END_DT = GETDATE(), + REAL_END_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + AND WORK_PLAN_YYMMDD = #{workPlanYymmdd} + + + + + UPDATE SX_GW0050 + SET WORK_END_DT = NOW(), + REAL_END_DT = NOW() + WHERE CORP_NO = #{corpNo} + AND USR_ID = #{usrId} + AND WORK_PLAN_YYMMDD = #{workPlanYymmdd} + + + + + + + + + diff --git a/backend/src/main/resources/mapper/tam/apvdoc_sql.xml b/backend/src/main/resources/mapper/tam/apvdoc_sql.xml new file mode 100644 index 0000000..86e26cb --- /dev/null +++ b/backend/src/main/resources/mapper/tam/apvdoc_sql.xml @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO SX_GW0090 ( + CORP_NO, APRVL_DOC_ID, APRVL_STUS_CD, APRVL_KIND_CD, + APLNT_ID, APLNT_CN, BZ_CN, OFFER_CN, BZ_DEPUTY_ID, + OFFER_DT, CMPL_DT, ATCH_NO, APRVL_CMPL_SNO, FINAL_APRVL_SNO, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{aprvlDocId}, '0001', #{aprvlKindCd}, + #{usrId}, #{aplntCn}, #{bzCn}, #{offerCn}, #{bzDeputyId}, + NULL, NULL, #{atchNo}, NULL, NULL, + #{usrId}, GETDATE(), #{usrId}, GETDATE() + ) + + + + + UPDATE SX_GW0090 + SET APLNT_CN = #{aplntCn}, + BZ_CN = #{bzCn}, + OFFER_CN = #{offerCn}, + BZ_DEPUTY_ID= #{bzDeputyId}, + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0090 + SET ATCH_NO = #{atchNo} + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0090 + SET APRVL_STUS_CD = '0002', + OFFER_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0090 + SET APRVL_STUS_CD = '0003', + CMPL_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0090 + SET APRVL_STUS_CD = '0004', + CMPL_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0090 + SET FINAL_APRVL_SNO = ( + SELECT MAX(AA.APRVL_SNO) FROM SX_GW0100 AA + WHERE AA.CORP_NO = #{corpNo} AND AA.APRVL_DOC_ID = #{aprvlDocId} + ) + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0090 + SET APRVL_CMPL_SNO = ( + SELECT MAX(CASE WHEN AA.APRVL_DT IS NOT NULL THEN AA.APRVL_SNO END) + FROM SX_GW0100 AA + WHERE AA.CORP_NO = #{corpNo} AND AA.APRVL_DOC_ID = #{aprvlDocId} + ) + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + DELETE FROM SX_GW0100 + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + + + + + INSERT INTO SX_GW0100 ( + CORP_NO, APRVL_DOC_ID, APRVL_SNO, APPR_ID, + APPR_STUS_CD, APRVL_DT, APPR_CN, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{aprvlDocId}, #{aprvlSno}, #{apprId}, + NULL, NULL, NULL, + #{usrId}, GETDATE(), #{usrId}, GETDATE() + ) + + + + + UPDATE SX_GW0100 + SET APPR_STUS_CD = #{apprStusCd}, + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + AND APRVL_SNO = #{aprvlSno} + + + + + UPDATE SX_GW0100 + SET APPR_STUS_CD = #{apprStusCd}, + APRVL_DT = GETDATE(), + APPR_CN = #{apprCn}, + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + AND APRVL_SNO = #{aprvlSno} + + + + + UPDATE SX_GW0100 + SET APPR_STUS_CD = '0002', + MODID = #{usrId}, + UPD_DT = GETDATE() + WHERE CORP_NO = #{corpNo} + AND APRVL_DOC_ID = #{aprvlDocId} + AND APRVL_SNO = ( + SELECT MIN(AA.APRVL_SNO) FROM SX_GW0100 AA + WHERE AA.CORP_NO = #{corpNo} + AND AA.APRVL_DOC_ID = #{aprvlDocId} + AND AA.APRVL_SNO > #{aprvlSno} + ) + + + + + DELETE FROM SX_GW0080 WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + DELETE FROM SX_GW0110 WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + DELETE FROM SX_GW0090 WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0090 SET DOC_FLAG = 'A' + WHERE CORP_NO = #{corpNo} AND APLNT_ID = #{usrId} AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0050 + SET WORK_END_DT = NULL + FROM SX_GW0050 g + LEFT JOIN SX_GW0090 d9 ON g.USR_ID = d9.APLNT_ID AND g.CORP_NO = d9.CORP_NO + LEFT JOIN SX_GW0080 d8 ON d9.APRVL_DOC_ID = d8.APRVL_DOC_ID AND d9.CORP_NO = d8.CORP_NO + WHERE g.CORP_NO = #{corpNo} + AND d9.APRVL_KIND_CD = '0003' + AND d9.APRVL_DOC_ID = #{aprvlDocId} + AND d8.APRVL_DOC_ID IS NOT NULL + AND g.WORK_PLAN_YYMMDD = d8.LATE_SKIPOFF_ABTI_OT_DATE + + + + + + + + + + INSERT INTO SX_GW0090 ( + CORP_NO, APRVL_DOC_ID, APRVL_STUS_CD, APRVL_KIND_CD, + APLNT_ID, APLNT_CN, BZ_CN, OFFER_CN, BZ_DEPUTY_ID, + OFFER_DT, CMPL_DT, ATCH_NO, APRVL_CMPL_SNO, FINAL_APRVL_SNO, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{aprvlDocId}, '0001', #{aprvlKindCd}, + #{usrId}, #{aplntCn}, #{bzCn}, #{offerCn}, #{bzDeputyId}, + NULL, NULL, #{atchNo}, NULL, NULL, + #{usrId}, NOW(), #{usrId}, NOW() + ) + + + + UPDATE SX_GW0090 + SET APLNT_CN = #{aplntCn}, BZ_CN = #{bzCn}, + OFFER_CN = #{offerCn}, BZ_DEPUTY_ID = #{bzDeputyId}, + MODID = #{usrId}, UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + + UPDATE SX_GW0090 + SET APRVL_STUS_CD = '0002', OFFER_DT = NOW() + WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + + UPDATE SX_GW0090 + SET APRVL_STUS_CD = '0003', CMPL_DT = NOW() + WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + + UPDATE SX_GW0090 + SET APRVL_STUS_CD = '0004', CMPL_DT = NOW() + WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + + INSERT INTO SX_GW0100 ( + CORP_NO, APRVL_DOC_ID, APRVL_SNO, APPR_ID, + APPR_STUS_CD, APRVL_DT, APPR_CN, + RGSTR_ID, RGST_DT, MODID, UPD_DT + ) VALUES ( + #{corpNo}, #{aprvlDocId}, #{aprvlSno}, #{apprId}, + NULL, NULL, NULL, + #{usrId}, NOW(), #{usrId}, NOW() + ) + + + + UPDATE SX_GW0100 + SET APPR_STUS_CD = #{apprStusCd}, APRVL_DT = NOW(), + APPR_CN = #{apprCn}, MODID = #{usrId}, UPD_DT = NOW() + WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} AND APRVL_SNO = #{aprvlSno} + + + diff --git a/backend/src/main/resources/mapper/tam/tam_sql.xml b/backend/src/main/resources/mapper/tam/tam_sql.xml new file mode 100644 index 0000000..ed62a9c --- /dev/null +++ b/backend/src/main/resources/mapper/tam/tam_sql.xml @@ -0,0 +1,417 @@ + + + + + + + + + + + + + + + MERGE INTO SX_GW0060 T + USING (SELECT 1 AS DUMMY) S + ON (T.CORP_NO = #{corpNo} AND T.USR_ID = #{usrId} AND T.YYVCT_YY = #{yyvctYy}) + WHEN NOT MATCHED THEN + INSERT (CORP_NO, USR_ID, YYVCT_YY, YYVCT_CNT, RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{usrId}, #{yyvctYy}, #{yyvctCnt}, #{loginUsrId}, GETDATE(), #{loginUsrId}, GETDATE()) + WHEN MATCHED THEN + UPDATE SET YYVCT_CNT = #{yyvctCnt}, MODID = #{loginUsrId}, UPD_DT = GETDATE(); + + + + + DELETE FROM SX_GW0060 + WHERE CORP_NO = #{corpNo} AND USR_ID = #{usrId} AND YYVCT_YY = #{yyvctYy} + + + + + + + + + + + + + + + + + + + INSERT INTO SX_GW0110 (CORP_NO, APRVL_DOC_ID, UPD_DATE, UPD_BF_WORK_CD, UPD_WORK_CD, + RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{aprvlDocId}, #{updDate}, #{updBfWorkCd}, #{updWorkCd}, + #{usrId}, GETDATE(), #{usrId}, GETDATE()) + + + + + INSERT INTO SX_GW0080 ( + CORP_NO, APRVL_DOC_ID, LATE_SKIPOFF_ABTI_OT_DATE, + LATE_SKIPOFF_OT_DT, LATE_SKIPOFF_OT_DT2, LATE_SKIPOFF_OT_MIN, + NML_WORK_RCNTN_YN, RGSTR_ID, RGST_DT, MODID, UPD_DT, REQUEST_DT, REQUEST_DT2 + ) VALUES ( + #{corpNo}, #{aprvlDocId}, #{lateSkipoffAbtiOtDate}, + CONVERT(DATETIME, NULLIF(#{lateSkipoffOtDt}, ''), 120), + CONVERT(DATETIME, NULLIF(#{lateSkipoffOtDt2}, ''), 120), + #{lateSkipoffOtMin}, ISNULL(#{nmlWorkRcntnYn}, 'N'), + #{usrId}, GETDATE(), #{usrId}, GETDATE(), #{requestDt}, #{requestDt2} + ) + + + + + DELETE FROM SX_GW0110 WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + + + DELETE FROM SX_GW0080 WHERE CORP_NO = #{corpNo} AND APRVL_DOC_ID = #{aprvlDocId} + + + + + + + + UPDATE SX_GW0090 SET DOC_FLAG = 'A' + WHERE CORP_NO = #{corpNo} AND APLNT_ID = #{usrId} AND APRVL_DOC_ID = #{aprvlDocId} + + + + + + + + + + + + + UPDATE SX_GW0100 SET DOC_FLAG = 'A' + WHERE CORP_NO = #{corpNo} AND APPR_ID = #{usrId} + AND APRVL_SNO = 1 AND APRVL_DOC_ID = #{aprvlDocId} + + + + + UPDATE SX_GW0050 + SET OUTING_MIN = ISNULL(OUTING_MIN, 0) + DATEDIFF(MI, A.LATE_SKIPOFF_OT_DT, A.LATE_SKIPOFF_OT_DT2), + OUTING_CNT = ISNULL(OUTING_CNT, 0) + 1 + FROM SX_GW0080 A + WHERE USR_ID = #{aplntId} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND WORK_PLAN_YYMMDD = A.LATE_SKIPOFF_ABTI_OT_DATE + + + + + + + + + + + + + + UPDATE W + SET W.OT_RCTN_YN = 'Y' + FROM SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + + UPDATE W + SET W.WORK_START_DT = A.REQUEST_DT + FROM SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + + UPDATE W + SET W.WORK_END_DT = A.REQUEST_DT, + W.EL_RCTN_YN = 'Y' + FROM SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + + UPDATE W + SET W.WORK_START_DT = A.REQUEST_DT, + W.WORK_END_DT = A.REQUEST_DT2 + FROM SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + + UPDATE W + SET W.WORK_CD = A.UPD_WORK_CD + FROM SX_GW0050 W + JOIN SX_GW0110 A + ON A.CORP_NO = W.CORP_NO + AND A.UPD_DATE = W.WORK_PLAN_YYMMDD + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + + UPDATE SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + SET W.OT_RCTN_YN = 'Y' + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + UPDATE SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + SET W.WORK_START_DT = A.REQUEST_DT + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + UPDATE SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + SET W.WORK_END_DT = A.REQUEST_DT, + W.EL_RCTN_YN = 'Y' + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + UPDATE SX_GW0050 W + JOIN SX_GW0080 A + ON A.CORP_NO = W.CORP_NO + AND A.LATE_SKIPOFF_ABTI_OT_DATE = W.WORK_PLAN_YYMMDD + SET W.WORK_START_DT = A.REQUEST_DT, + W.WORK_END_DT = A.REQUEST_DT2 + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + UPDATE SX_GW0050 W + JOIN SX_GW0110 A + ON A.CORP_NO = W.CORP_NO + AND A.UPD_DATE = W.WORK_PLAN_YYMMDD + SET W.WORK_CD = A.UPD_WORK_CD + WHERE A.CORP_NO = #{corpNo} + AND A.APRVL_DOC_ID = #{aprvlDocId} + AND W.USR_ID = #{aplntId} + + + + + + + + + + INSERT INTO SX_GW0060 (CORP_NO, USR_ID, YYVCT_YY, YYVCT_CNT, RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{usrId}, #{yyvctYy}, #{yyvctCnt}, #{loginUsrId}, NOW(), #{loginUsrId}, NOW()) + ON DUPLICATE KEY UPDATE + YYVCT_CNT = #{yyvctCnt}, MODID = #{loginUsrId}, UPD_DT = NOW() + + + + INSERT INTO SX_GW0110 (CORP_NO, APRVL_DOC_ID, UPD_DATE, UPD_BF_WORK_CD, UPD_WORK_CD, + RGSTR_ID, RGST_DT, MODID, UPD_DT) + VALUES (#{corpNo}, #{aprvlDocId}, #{updDate}, #{updBfWorkCd}, #{updWorkCd}, + #{usrId}, NOW(), #{usrId}, NOW()) + + + diff --git a/backend/src/main/resources/mapper/wplan/wplan_sql.xml b/backend/src/main/resources/mapper/wplan/wplan_sql.xml new file mode 100644 index 0000000..a62b9cd --- /dev/null +++ b/backend/src/main/resources/mapper/wplan/wplan_sql.xml @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/wtime/wtime_sql.xml b/backend/src/main/resources/mapper/wtime/wtime_sql.xml new file mode 100644 index 0000000..9074795 --- /dev/null +++ b/backend/src/main/resources/mapper/wtime/wtime_sql.xml @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + diff --git a/backend/tls-local.security b/backend/tls-local.security new file mode 100644 index 0000000..00ebc97 --- /dev/null +++ b/backend/tls-local.security @@ -0,0 +1,2 @@ +# 로컬 개발 전용 - TLS 1.0 허용 (서버가 TLS 1.0만 지원) +jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..90d180d --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,34 @@ +version: '3.9' + +# 로컬 개발용 - 인프라 서비스만 실행 (앱 제외) +# 사용법: docker-compose -f docker-compose.dev.yml up -d + +services: + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis-dev-data:/data + command: redis-server --appendonly yes + networks: + - gw-dev + + rabbitmq: + image: rabbitmq:3.13-management + ports: + - "5672:5672" + - "15672:15672" # 관리 콘솔: http://localhost:15672 (guest/guest) + volumes: + - rabbitmq-dev-data:/var/lib/rabbitmq + networks: + - gw-dev + +volumes: + redis-dev-data: + rabbitmq-dev-data: + +networks: + gw-dev: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..343dd48 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,101 @@ +version: '3.9' + +services: + + nginx: + image: nginx:1.26 + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - upload-data:/data/uploads:ro + depends_on: + - frontend + - backend + networks: + - gw-network + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + environment: + - NEXT_PUBLIC_API_URL=/api + depends_on: + - backend + networks: + - gw-network + restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: Dockerfile + env_file: + - .env + environment: + - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-mssql} + - DB_URL=${DB_URL} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - REDIS_HOST=${REDIS_HOST:-redis} + - REDIS_PORT=${REDIS_PORT:-6379} + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - RABBITMQ_HOST=${RABBITMQ_HOST:-rabbitmq} + - RABBITMQ_PORT=${RABBITMQ_PORT:-5672} + - RABBITMQ_USER=${RABBITMQ_USER:-guest} + - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD:-guest} + - FILE_UPLOAD_PATH=${FILE_UPLOAD_PATH:-/data/uploads} + volumes: + - upload-data:/data/uploads + depends_on: + - redis + - rabbitmq + networks: + - gw-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis-data:/data + networks: + - gw-network + restart: unless-stopped + command: > + redis-server + --appendonly yes + ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}} + + rabbitmq: + image: rabbitmq:3.13-management + ports: + - "5672:5672" + - "15672:15672" # 관리 콘솔 + environment: + - RABBITMQ_DEFAULT_USER=${RABBITMQ_USER:-guest} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD:-guest} + volumes: + - rabbitmq-data:/var/lib/rabbitmq + networks: + - gw-network + restart: unless-stopped + +volumes: + redis-data: + rabbitmq-data: + upload-data: + +networks: + gw-network: + driver: bridge diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..966485d --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=http://localhost:8080 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..6a28dcf --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.next/ +out/ +.env +.env.local +.env.production +pnpm-lock.yaml diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e622a0d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-alpine AS builder +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate + +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm build + +FROM node:22-alpine +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@latest --activate + +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +ENV NODE_ENV=production +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..bc0a03a --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,55 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + // Docker 배포용 standalone 빌드 + output: process.env.NODE_ENV === 'production' ? 'standalone' : undefined, + + // API Gateway로 요청 프록시 + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080'}/api/:path*`, + }, + ] + }, + + // 구 Grails URL → 새 Next.js 경로 리다이렉트 (DB의 URL 값 그대로 유지) + async redirects() { + return [ + // ── 게시판 ────────────────────────────────── + { source: '/board_:id', destination: '/board/:id', permanent: false }, + + // ── 결재 (TAM0020: brunch 타입별 → 신청 목록) ── + { source: '/tam0020/brunch/:type', destination: '/tam/0020', permanent: false }, + { source: '/tam0020', destination: '/tam/0020', permanent: false }, + { source: '/tam0010', destination: '/tam/0010', permanent: false }, + { source: '/tam0030', destination: '/tam/0030', permanent: false }, + { source: '/tam0040', destination: '/tam/0040', permanent: false }, + + // ── 근무계획 ───────────────────────────────── + { source: '/wplan0010', destination: '/wplan/0010', permanent: false }, + { source: '/wplan0020', destination: '/wplan/0020', permanent: false }, + { source: '/wplan0030', destination: '/wplan/0030', permanent: false }, + + // ── 근무시간 ───────────────────────────────── + { source: '/wtime0010', destination: '/wtime/0010', permanent: false }, + { source: '/wtime0030', destination: '/wtime/0030', permanent: false }, + + // ── 환경설정 ───────────────────────────────── + { source: '/envset0010', destination: '/envset/users', permanent: false }, + { source: '/envset0020', destination: '/envset/codes', permanent: false }, + { source: '/envset0030/:path*', destination: '/envset/codes-view',permanent: false }, + { source: '/envset0040', destination: '/envset/workcd', permanent: false }, + { source: '/envset0050', destination: '/envset/menus', permanent: false }, + + // ── 비밀번호변경 ────────────────────────────── + { source: '/main0020', destination: '/my/change-pw', permanent: false }, + + // ── FedEx ──────────────────────────────────── + { source: '/fedex0010', destination: '/fedex/0010', permanent: false }, + ] + }, +} + +export default nextConfig diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3d252bd --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,49 @@ +{ + "name": "gw-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toast": "^1.2.4", + "@radix-ui/react-tooltip": "^1.1.6", + "@tanstack/react-query": "^5.64.1", + "@tanstack/react-query-devtools": "^5.64.1", + "@toast-ui/editor": "^3.2.2", + "@toast-ui/react-editor": "^3.2.3", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.469.0", + "next": "15.1.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "tailwind-merge": "^2.6.0", + "zod": "^3.24.1", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/node": "^22.10.7", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "autoprefixer": "^10.4.20", + "eslint": "^9.18.0", + "eslint-config-next": "15.1.3", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3" + }, + "packageManager": "pnpm@9.15.3" +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..393a10f --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + +export default config diff --git a/frontend/src/app/(main)/board/[boardType]/[postSno]/edit/page.tsx b/frontend/src/app/(main)/board/[boardType]/[postSno]/edit/page.tsx new file mode 100644 index 0000000..3082951 --- /dev/null +++ b/frontend/src/app/(main)/board/[boardType]/[postSno]/edit/page.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { use } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getPostInfoForEdit } from '@/lib/api/board'; +import PostForm from '@/components/board/PostForm'; + +export default function BoardEditPage({ + params, +}: { + params: Promise<{ boardType: string; postSno: string }>; +}) { + const { boardType, postSno } = use(params); + + const { data, isLoading } = useQuery({ + queryKey: ['board-edit', boardType, postSno], + queryFn: () => getPostInfoForEdit(boardType, Number(postSno)), + }); + + if (isLoading) return

불러오는 중...
; + if (!data) return
게시물이 없습니다.
; + + return ; +} diff --git a/frontend/src/app/(main)/board/[boardType]/[postSno]/page.tsx b/frontend/src/app/(main)/board/[boardType]/[postSno]/page.tsx new file mode 100644 index 0000000..e480ac9 --- /dev/null +++ b/frontend/src/app/(main)/board/[boardType]/[postSno]/page.tsx @@ -0,0 +1,178 @@ +'use client'; + +import { useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + getPostInfo, + deletePost, + getCommentList, + insertComment, + deleteComment, +} from '@/lib/api/board'; +import { downloadFile, downloadZip } from '@/lib/api/attach'; +import { useAuthStore } from '@/lib/store/authStore'; + +export default function BoardDetailPage() { + const params = useParams(); + const router = useRouter(); + const qc = useQueryClient(); + const usrId = useAuthStore((s) => s.user?.usrId); + const boardType = params.boardType as string; + const postSno = Number(params.postSno); + + const [cmmtInput, setCmmtInput] = useState(''); + + const { data: post, isLoading } = useQuery({ + queryKey: ['board-detail', boardType, postSno], + queryFn: () => getPostInfo(boardType, postSno), + }); + + const { data: comments = [] } = useQuery({ + queryKey: ['board-comments', boardType, postSno], + queryFn: () => getCommentList(boardType, postSno), + }); + + const addCommentMut = useMutation({ + mutationFn: () => insertComment(boardType, postSno, cmmtInput), + onSuccess: () => { + setCmmtInput(''); + qc.invalidateQueries({ queryKey: ['board-comments', boardType, postSno] }); + qc.invalidateQueries({ queryKey: ['board-detail', boardType, postSno] }); + }, + }); + + const delCommentMut = useMutation({ + mutationFn: (cmmtSno: number) => deleteComment(boardType, postSno, cmmtSno), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['board-comments', boardType, postSno] }); + }, + }); + + const handleDeletePost = async () => { + if (!confirm('게시물을 삭제하시겠습니까?')) return; + try { + await deletePost(boardType, postSno); + router.push(`/board/${boardType}`); + } catch (e: any) { + alert(e?.response?.data?.message ?? '삭제 중 오류가 발생했습니다.'); + } + }; + + if (isLoading) return
불러오는 중...
; + if (!post) return
게시물이 없습니다.
; + + const isAuthor = usrId === post.ctusrId; + + return ( +
+ {/* 제목 영역 */} +
+

{post.bbsTitleNm}

+
+ 작성자: {post.ctusrNm} + 등록일: {post.rgstDate} + 조회: {post.inqrCnt} +
+
+ + {/* 본문 */} +
+ + {/* 첨부파일 */} + {post.etcFileList && post.etcFileList.length > 0 && ( +
+

첨부파일

+
    + {post.etcFileList.map((f: any) => ( +
  • + +
  • + ))} +
+ {post.etcFileList.length > 1 && ( + + )} +
+ )} + + {/* 버튼 */} +
+ + {isAuthor && ( + <> + + + + )} +
+ + {/* 댓글 */} +
+

댓글 {comments.length}개

+
    + {comments.map((c) => ( +
  • +
    + {c.cmmtCtusrNm} + {c.rgstDate} + {c.cmmtCn} +
    + {usrId === c.cmmtCtusrId && ( + + )} +
  • + ))} +
+
+