Merge branch 'featrue/0526-update'

This commit is contained in:
Yesol Choi
2025-05-27 14:38:20 +09:00
3 changed files with 747 additions and 22 deletions

View File

@@ -2,13 +2,12 @@
import {getContractList} from "/src/service/contract"; import {getContractList} from "/src/service/contract";
import regex, {formatCurrency} from "/@src/utils/common/regex.ts"; import regex, {formatCurrency} from "/@src/utils/common/regex.ts";
export type MinimalTheme = 'darker' | 'light'
const emits = defineEmits(['on-search', 'on-tr-click']) const emits = defineEmits(['on-search', 'on-tr-click'])
const selUser = ref() const selUser = ref()
const masks = ref({ const masks = ref({
modelValue: 'YYYY-MM-DD', modelValue: 'YYYY-MM-DD',
}) })
const router = useRouter()
const selectedCode = ref() const selectedCode = ref()
@@ -23,6 +22,8 @@ const data = reactive({
onBeforeMount(async () => { onBeforeMount(async () => {
await getContractListView() await getContractListView()
const userSession = useUserSession()
params.sessionUser = userSession.user.data
}) })
async function getContractListView() { async function getContractListView() {
@@ -39,6 +40,7 @@ async function getContractListView() {
} }
const params = reactive({ const params = reactive({
sessionUser:'',
flexColumn: [ flexColumn: [
{ key: 'cateNm', label: '분야' }, { key: 'cateNm', label: '분야' },
{ key: 'title', label: '계약명' }, { key: 'title', label: '계약명' },
@@ -51,7 +53,14 @@ const params = reactive({
], ],
}) })
function getContractDetail(){
//contStatCd 결재상태 코드 [계약전:0000, 계약종료:0100, ]
if(params.sessionUser.sabun == arguments[0].regSabun && arguments[0].contStatCd == '0000'){
router.push({ path: '/app/contractUpdate', state: { key: arguments[0].contNo}})
}else{
//router.push({ path: '/app/priceDetail', state: { key: arguments[0].prcsNo }})
}
}
</script> </script>
<template> <template>
@@ -179,6 +188,7 @@ const params = reactive({
:clickable="true" :clickable="true"
:rounded="true" :rounded="true"
:compact="true" :compact="true"
@rowClick="getContractDetail"
/> />
</div> </div>
</div> </div>

View File

@@ -0,0 +1,610 @@
<script setup lang="ts">
import axios from 'axios'
import { getContractDetail, updateContract, saveContract } from "/@src/service/contract.ts";
import { getDetailPrcs } from "/@src/service/priceApi.ts";
onBeforeMount(async ()=>{
const result = await getContractDetail(history.state.key)
getDetailList(result)
})
const registerFormOpen = ref(false)
const loading = ref(false)
const notyf = useNotyf()
const router = useRouter()
const params = reactive({
cateCd: '',
contNo: '',
title: '',
compNm: '',
signDt: '',
contAmt: '',
contStatCd: '',
contStat: '',
regsabun: '',
regNm: '',
regDt: '',
reason: '',
page: 1,
row: 10,
flexColumn: [],
modalColumn: [],
})
const completedPriceDataParams = reactive({
prcsNo:'',
cateCd:'',
cateNm:'',
bizNo:'',
compNm:'',
title:'',
content:'',
regSdat:'',
regEdat:'',
regSabun:'',
regNm:'',
regDt:'',
contAmt:'',
stCd:'',
stNm:'',
svyDt:'',
reason:'',
estimates: [],
page: 1,
row: 5,
})
params.modalColumn = [
{ key: 'cateNm', label: '분야' },
{ key: 'title', label: '제목' },
{ key: 'regNm', label: '담당자' },
{ key: 'stNm', label: '등록상태' },
{ key: 'title', label: '비고' },
{ key: 'regNm', label: '선택' },
]
const selectedCode = ref()
const priceSearchCheckBoxStatus = ref(false)
const data = reactive({
contractData: [],
completedPriceSearchData: [],
})
const isLoading = ref(false)
watch(registerFormOpen, async (isOpen) => {
if (isOpen) {
isLoading.value = true
// error.value = null
try {
const priceSearchDataRespone = await axios.get('/api/cont/prcs')
data.completedPriceSearchData = Array.isArray(priceSearchDataRespone.data) ? priceSearchDataRespone.data : []
}
catch (error) {
console.log(error)
data.priceData = []
}
}
})
function getDetailList(arg){
console.log("arg",arg)
completedPriceDataParams.prcsNo = arg.prcsNo
selectedCode.value = arg.cateCd
completedPriceDataParams.bizNo = arg.bizNo
completedPriceDataParams.compNm = arg.compNm
completedPriceDataParams.title = arg.title
completedPriceDataParams.regSdat = arg.contSdat // 계약기간
completedPriceDataParams.regEdat = arg.contEdat
completedPriceDataParams.contAmt = arg.contAmt
// completedPriceDataParams.svyDt = arg.signDt 계약체결일 todo
completedPriceDataParams.reason = arg.reason
}
function formatMonthDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).replace(/\./g, '-').replace(/\s/g, '').replace(/-$/, '')
}
function getDateDiff(start, end) {
if (!start || !end) return null
const startDate = new Date(start)
const endDate = new Date(end)
const diff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24))
return diff >= 0 ? diff + 1 : null
}
const contractPeriod = computed(() => {
const start = completedPriceDataParams.regSdat
const end = completedPriceDataParams.regEdat
const startStr = formatMonthDate(start)
const endStr = formatMonthDate(end)
const diff = getDateDiff(start, end)
if (startStr && endStr && diff) {
return `${startStr} ~ ${endStr} (${diff}일)`
}
else if (startStr && endStr) {
return `${startStr} ~ ${endStr}`
}
else if (startStr) {
return `${startStr} ~`
}
else if (endStr) {
return `~ ${endStr}`
}
else {
return ''
}
})
function handlePriceRowClick(row) {
completedPriceDataParams.prcsNo = row.prcsNo || ''
completedPriceDataParams.cateCd = row.cateCd || ''
completedPriceDataParams.cateNm = row.cateNm || ''
completedPriceDataParams.title = row.title || ''
completedPriceDataParams.content = row.content || ''
completedPriceDataParams.regSdat = row.regSdat || ''
completedPriceDataParams.regEdat = row.regEdat || ''
completedPriceDataParams.regSabun = row.regSabun || ''
completedPriceDataParams.regNm = row.regNm || ''
completedPriceDataParams.regDt = row.regDt || ''
completedPriceDataParams.stCd = row.stCd || ''
completedPriceDataParams.stNm = row.stNm || ''
completedPriceDataParams.svyDt = row.svyDt || ''
completedPriceDataParams.reason = row.reason || ''
selectedCode.value = row.cateCd || ''
completedPriceDataParams.estimates = row.estimates || []
if (row.estimates && row.estimates.length > 0) {
const minEstimate = row.estimates.reduce((min, curr) => curr.amt < min.amt ? curr : min, row.estimates[0])
completedPriceDataParams.bizNo = minEstimate.bizNo || ''
completedPriceDataParams.compNm = minEstimate.compNm || ''
completedPriceDataParams.contAmt = minEstimate.amt || ''
} else {
completedPriceDataParams.bizNo = ''
completedPriceDataParams.compNm = ''
completedPriceDataParams.contAmt = ''
notyf.error("견적서가 없습니다.")
}
registerFormOpen.value = false
priceSearchCheckBoxStatus.value = !!row
console.log(row)
console.log(completedPriceDataParams.contAmt)
}
const showFileInputs = ref(false)
const fileInputs = ref([
{ file: null, description: '시행 품의문' },
{ file: null, description: '소액수의계약서' },
{ file: null, description: '수의계약 체결 제한 여부 확인서' },
{ file: null, description: '퇴직자 재직여부 확인서' },
{ file: null, description: '인지세 납부확인서' },
{ file: null, description: '정부권장정책 이행 구매 검토서' },
{ file: null, description: '기타' }
])
function handleFileChange(e, idx) {
const files = e.target.files
if (files && files.length > 0) {
fileInputs.value[idx].file = files[0]
} else {
fileInputs.value[idx].file = null
}
}
const saveContOne = async () => {
let res = null
try{
loading.value = true
// if (!validation()) {
// return;
// }
const paramsCont ={
prcsNo: completedPriceDataParams.prcsNo,
bizNo: completedPriceDataParams.bizNo,
compNm: completedPriceDataParams.compNm,
title: completedPriceDataParams.title,
regSdat: formatMonthDate(completedPriceDataParams.regSdat),
regEdat: formatMonthDate(completedPriceDataParams.regEdat),
contAmt: completedPriceDataParams.contAmt,
signDt: formatMonthDate(completedPriceDataParams.svyDt),
reason: completedPriceDataParams.reason,
exeYn: true, // 가격조사 예외여부 확인필요
// cateCd: completedPriceDataParams.cateCd,
// cateNm: completedPriceDataParams.cateNm,
// content: completedPriceDataParams.content,
// regSabun: completedPriceDataParams.regSabun,
// regNm: completedPriceDataParams.regNm,
// regDt: formatMonthDate(completedPriceDataParams.regDt),
// stCd: completedPriceDataParams.stCd,
// stNm: completedPriceDataParams.stNm,
}
res = await updateContract(paramsCont)
if(res.request.status == '200'){
notyf.primary('등록 되었습니다.')
router.push({path: '/app/contractManagement'})
}
}catch(e){
notyf.error(e.message)
}finally {
loading.value = false
}
}
const moveList = () => {
router.push('/app/contractManagement')
}
</script>
<template>
<div class="page-content is-navbar-lg">
<div class="datatable-wrapper">
<div class="table-container">
<table class="table datatable-table is-fullwidth">
<colgroup>
<col style="width: 10%;">
<col style="width: 20%;">
<col style="width: 20%;">
<col style="width: 10%;">
<col style="width: 10%;">
<col style="width: 20%;">
<col style="width: 10%;">
</colgroup>
<tbody>
<tr>
<td>분야</td>
<td>
<span class="colum">
<VField class="">
<VCodeSelect
v-model="selectedCode"
cd_grp="5"
/></VField>
</span>
</td>
<td>
<VButton
color="primary"
@click="registerFormOpen = true"
>
가격조사 가져오기
</VButton>
<VModal
is="form"
:open="registerFormOpen"
title="계약관리 등록"
size="contract-big"
actions="right"
>
<template #content>
<div class="modal-form">
<ComVFlexTable
:data="data.completedPriceSearchData"
:columns="params.modalColumn"
:compact="true"
:separators="true"
@row-click="handlePriceRowClick"
clickable
/>
</div>
</template>
</VModal>
</td>
<td>
<VField class="is-flex">
<VControl raw subcontrol>
<VCheckbox
label="가격조사여부"
color="info"
v-model="priceSearchCheckBoxStatus"
/>
</VControl>
</VField>
</td>
<td colspan="3">
<div class="column is-fullhd">
<VField>
<VControl>
<input
v-model="completedPriceDataParams.reason"
class="input custom-text-filter"
placeholder="가격조사 안했을 시 예외 사유 입력(필수)"
>
</VControl>
</VField>
</div>
</td>
</tr>
<tr>
<td>계약명</td>
<td colspan="6">
<VField>
<VControl>
<input
v-model="completedPriceDataParams.title"
class="input custom-text-filter"
placeholder="계약명"
>
</VControl>
</VField>
</td>
</tr>
<tr>
<td>계약상대자</td>
<td>
<VField>
<VControl>
<input
v-model="completedPriceDataParams.bizNo"
class="input custom-text-filter"
placeholder="사업자번호"
>
</VControl>
</VField>
</td>
<td>
<VField>
<VControl>
<input
v-model="completedPriceDataParams.compNm"
class="input custom-text-filter"
placeholder="업체명"
>
</VControl>
</VField>
</td>
<td>
<VButton color="warning">
부정당 확인
</VButton>
</td>
<td>
<VButton color="success">
정상
</VButton>
</td>
<td>
<VButton color="warning">
분할계약 확인
</VButton>
</td>
<td>
<VButton color="success">
정상
</VButton>
</td>
</tr>
<tr>
<td>계약체결일</td>
<td>
<VField>
<VControl>
<input
:value="formatMonthDate(completedPriceDataParams.regSdat)"
class="input custom-text-filter"
placeholder="계약체결일"
>
</VControl>
</VField>
</td>
<td>
<VButton
color="primary"
>
</VButton>
<VModal
actions="center"
title="계약금액"
>
<template #content>
<VPlaceholderSection
title="Go Premium"
subtitle="Unlock more features and business tools by going premium"
/>
</template>
<template #action>
<VButton color="primary" raised>
등록
</VButton>
</template>
</VModal>
</td>
<td colspan="2">
<VField>
<VControl>
<input
v-model="completedPriceDataParams.contAmt"
class="input custom-text-filter"
placeholder="금액"
>
</VControl>
</VField>
</td>
<td colspan="1">
<span class="colum">
<VField>
<VSelect>
<VOption value="">
수의계약 사유
</VOption>
</VSelect>
</VField>
</span>
</td>
<td>
<VButton>근거</VButton>
</td>
</tr>
<tr>
<td>계약기간</td>
<td colspan="1">
<div>
<div>
<VDatePicker
v-model="completedPriceDataParams.regSdat"
color="green"
trim-weeks
>
<template #default="{ inputValue, inputEvents }">
<VField>
<VControl icon="lucide:calendar">
<input
class="input v-input"
type="text"
:value="inputValue"
placeholder="시작일"
v-on="inputEvents"
>
</VControl>
</VField>
</template>
</VDatePicker>
</div>
</div>
</td>
<td colspan="1">
<div class="">
<div>
<VDatePicker
v-model="completedPriceDataParams.regEdat"
color="green"
trim-weeks
>
<template #default="{ inputValue, inputEvents }">
<VField>
<VControl icon="lucide:calendar">
<input
class="input v-input"
type="text"
:value="inputValue"
placeholder="종료일"
v-on="inputEvents"
>
</VControl>
</VField>
</template>
</VDatePicker>
</div>
</div>
</td>
<td colspan="5">
계약기간 : {{ contractPeriod }}
</td>
</tr>
<tr>
<td>첨부파일</td>
<td colspan="1">
<VButton color="info" @click="showFileInputs = !showFileInputs">
등록
</VButton>
</td>
<td colspan="5">
<!-- 첨부파일 입력영역: 등록 버튼 클릭 토글 -->
<div v-if="showFileInputs" class="file-upload-list" style="margin-top:10px;">
<div
v-for="(input, idx) in fileInputs"
:key="idx"
style="display: flex; align-items: center; margin-bottom: 8px;"
>
<!-- 파일선택 -->
<label class="file-label" style="margin-right: 10px;">
<input
type="file"
class="file-input"
@change="e => handleFileChange(e, idx)"
/>
<span class="file-cta">파일선택</span>
</label>
<span style="flex:1; margin-right: 10px;">{{ input.file ? input.file.name : '첨부된 파일 없음' }}</span>
<span style="flex:2; color: #666;">{{ input.description }}</span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="bottom-button">
<VButton @click="saveContOne"> </VButton>
<VButton @click="moveList"> </VButton>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.table tbody td {
color: var(--smoke-white);
}
.datatable-table {
td{
font-family: var(--font),serif;
vertical-align: middle;
padding: 4px 12px;
border-bottom: 1px solid var(--fade-grey);
}
td:nth-child(1) {
background-color: var(--primary);
text-align: center;
}
tr:nth-child(3) {
text-align: center;
}
tr td button{
width: 100%;
}
tr:nth-child(5) > td:nth-child(4) {
color: black;
}
}
.bottom-button {
text-align: center;
button {
background-color: cornflowerblue;
margin: 10px;
font-weight: bold;
border-color: var(--primary);
color: white;
}
}
button:nth-child(2) {
background-color: #AB9A6c;
}
button:nth-child(3) {
background-color: silver;
}
.field {
margin-bottom: 0;
}
.file-label {
cursor: pointer;
}
.file-input {
display: none;
}
.file-cta {
background: #eee;
padding: 4px 12px;
border-radius: 4px;
}
</style>

View File

@@ -1,26 +1,37 @@
import axios from 'axios' import axios from 'axios'
/** /**
* 계약관리 조회(페이징) * 계약관리 수정
* @property {string} params.page - 페이지 * @param {object} params
* @property {string} params.row - 아이템갯수 * @property {string} params.contNo -
* * @property {string} params.bizNo -
*/`` * @property {string} params.prcsNo -
export async function getContractList(params = {}) { * @property {string} params.title -
try { * @property {string} params.compNm -
const result = await axios.get(`/api/cont/page`,params) * @property {string} params.signDt -
return result.data * @property {string} params.contSdat -
* @property {string} params.contEdat -
* @property {string} params.amt -
* @property {string} params.excYn -
* @property {string} params.reason -
* @property {string} params.contAtts{fileOrd,logiFnm,data} -
* @returns
*/
export async function updateContract(params = {}) {
try {
const result = await axios.put(`/api/cont`,params)
return result
} catch (e) { } catch (e) {
if (e.response) { const serverError = e.response?.data;
if (e.response.status >= 500) {
throw new Error('서버 오류가 발생했습니다.') const message = typeof serverError?.body === 'string'
} else if (e.response.status >= 400) { ? serverError.body
throw new Error('잘못된 요청입니다.') : 'Unknown error occurred';
} else if (e.response._data && e.response._data.message) {
throw new Error(e.response._data.message) const error = new Error(message); // ✅ 반드시 string만 넣기! 아니면 객체가 문자열로 나옴
} error.code = serverError?.code;
} error.errTime = serverError?.errTime;
throw new Error(e.message || '알 수 없는 오류') throw error;
} }
} }
@@ -41,6 +52,100 @@ export async function saveContract(params = {}) {
} catch (e) { } catch (e) {
const serverError = e.response?.data; const serverError = e.response?.data;
const message = typeof serverError?.body === 'string'
? serverError.body
: 'Unknown error occurred';
const error = new Error(message); // ✅ 반드시 string만 넣기! 아니면 객체가 문자열로 나옴
error.code = serverError?.code;
error.errTime = serverError?.errTime;
throw error;
}
}
/**
* 계약관리 상세조회
* @param {object} params
* @property {string} params.contNo -계약번호
* @returns
*/
export async function getContractDetail(contNo) {
try {
const result = await axios.get(`/api/cont/${contNo}`)
return result.data
} catch (e) {
const serverError = e.response?.data;
const message = typeof serverError?.body === 'string'
? serverError.body
: 'Unknown error occurred';
const error = new Error(message); // ✅ 반드시 string만 넣기! 아니면 객체가 문자열로 나옴
error.code = serverError?.code;
error.errTime = serverError?.errTime;
throw error;
}
}
/**
* 계약관리 조회(페이징)
* @property {string} params.page - 페이지
* @property {string} params.row - 아이템갯수
*
*/``
export async function getContractList(params = {}) {
try {
const result = await axios.get(`/api/cont/page`,params)
return result.data
} catch (e) {
if (e.response) {
if (e.response.status >= 500) {
throw new Error('서버 오류가 발생했습니다.')
} else if (e.response.status >= 400) {
throw new Error('잘못된 요청입니다.')
} else if (e.response._data && e.response._data.message) {
throw new Error(e.response._data.message)
}
}
throw new Error(e.message || '알 수 없는 오류')
}
}
/**
* 계약관리 - 가격조사 가져오기
* @param {object} params
* @returns
*/
export async function getPriceList() {
try {
const result = await axios.get(`/api/cont/prcs`)
return result.data
} catch (e) {
const serverError = e.response?.data;
const message = typeof serverError?.body === 'string'
? serverError.body
: 'Unknown error occurred';
const error = new Error(message); // ✅ 반드시 string만 넣기! 아니면 객체가 문자열로 나옴
error.code = serverError?.code;
error.errTime = serverError?.errTime;
throw error;
}
}
/**
* 계약관리 회수 처리
* @param {object} contNo
* @returns
*/
export async function getPrice(contNo) {
try {
const result = await axios.put(`/api/cont/ret/${contNo}`)
return result.data
} catch (e) {
const serverError = e.response?.data;
const message = typeof serverError?.body === 'string' const message = typeof serverError?.body === 'string'
? serverError.body ? serverError.body
: 'Unknown error occurred'; : 'Unknown error occurred';