Merge branch 'refs/heads/main' into featrue/0526-update

This commit is contained in:
Yesol Choi
2025-05-27 08:34:13 +09:00
7 changed files with 322 additions and 236 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import axios from 'axios'
import notyf from "/@src/plugins/notyf.ts";
const registerFormOpen = ref(false)
const params = reactive({
@@ -22,6 +23,29 @@ const params = reactive({
modalColumn: [],
})
const completedPriceDataParams = reactive({
prcsNo:'',
cateCd:'',
cateNm:'',
bizNo:'',
compNm:'',
title:'',
content:'',
regSdat:'',
regEdat:'',
regSabun:'',
regNm:'',
regDt:'',
amt:'',
stCd:'',
stNm:'',
svyDt:'',
reason:'',
estimates: [],
page: 1,
row: 5,
})
params.modalColumn = [
{ key: 'cateNm', label: '분야' },
{ key: 'title', label: '제목' },
@@ -30,11 +54,13 @@ params.modalColumn = [
{ key: 'title', label: '비고' },
{ key: 'regNm', label: '선택' },
]
const selectedCode = ref()
const priceSearchCheckBoxStatus = ref(false)
const data = reactive({
contractData: [],
priceSearchData: [],
completedPriceSearchData: [],
})
const isLoading = ref(false)
@@ -44,8 +70,7 @@ watch(registerFormOpen, async (isOpen) => {
// error.value = null
try {
const priceSearchDataRespone = await axios.get('/api/cont/prcs')
console.log(priceSearchDataRespone.data)
data.priceSearchData = Array.isArray(priceSearchDataRespone.data) ? priceSearchDataRespone.data : []
data.completedPriceSearchData = Array.isArray(priceSearchDataRespone.data) ? priceSearchDataRespone.data : []
}
catch (error) {
console.log(error)
@@ -73,8 +98,8 @@ function getDateDiff(start, end) {
}
const contractPeriod = computed(() => {
const start = params.regSdt
const end = params.regEdt
const start = completedPriceDataParams.regSdat
const end = completedPriceDataParams.regEdat
const startStr = formatDate(start)
const endStr = formatDate(end)
const diff = getDateDiff(start, end)
@@ -95,6 +120,67 @@ const contractPeriod = computed(() => {
}
})
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.amt = minEstimate.amt || ''
} else {
completedPriceDataParams.bizNo = ''
completedPriceDataParams.compNm = ''
completedPriceDataParams.amt = ''
notyf.error("견적서가 없습니다.")
}
registerFormOpen.value = false
priceSearchCheckBoxStatus.value = !!row
console.log(row)
console.log(completedPriceDataParams.amt)
}
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
}
}
</script>
<template>
@@ -116,10 +202,10 @@ const contractPeriod = computed(() => {
<td>분야</td>
<td>
<span class="colum">
<VField class="pr-2">
<VField class="">
<VCodeSelect
v-model="selectedCode"
cd_grp="5/"
cd_grp="5"
/></VField>
</span>
</td>
@@ -136,28 +222,19 @@ const contractPeriod = computed(() => {
title="계약관리 등록"
size="contract-big"
actions="right"
@submit.prevent="registerFormOpen = false"
@close="registerFormOpen = false"
>
<template #content>
<div class="modal-form">
<ComVFlexTable
:data="data.priceSearchData"
:data="data.completedPriceSearchData"
:columns="params.modalColumn"
:compact="true"
:separators="true"
@row-click="handlePriceRowClick"
clickable
/>
</div>
</template>
<template #action>
<VButton
type="submit"
color="primary"
raised
>
Save Changes
</VButton>
</template>
</VModal>
</td>
<td>
@@ -166,6 +243,7 @@ const contractPeriod = computed(() => {
<VCheckbox
label="가격조사여부"
color="info"
v-model="priceSearchCheckBoxStatus"
/>
</VControl>
</VField>
@@ -175,7 +253,7 @@ const contractPeriod = computed(() => {
<VField>
<VControl>
<input
v-model="params.reason"
v-model="completedPriceDataParams.reason"
class="input custom-text-filter"
placeholder="가격조사 안했을 시 예외 사유 입력(필수)"
>
@@ -190,7 +268,7 @@ const contractPeriod = computed(() => {
<VField>
<VControl>
<input
v-model="params.title"
v-model="completedPriceDataParams.title"
class="input custom-text-filter"
placeholder="계약명"
>
@@ -204,7 +282,7 @@ const contractPeriod = computed(() => {
<VField>
<VControl>
<input
v-model="params.regNm"
v-model="completedPriceDataParams.bizNo"
class="input custom-text-filter"
placeholder="사업자번호"
>
@@ -215,7 +293,7 @@ const contractPeriod = computed(() => {
<VField>
<VControl>
<input
v-model="params.compNm"
v-model="completedPriceDataParams.compNm"
class="input custom-text-filter"
placeholder="업체명"
>
@@ -249,7 +327,7 @@ const contractPeriod = computed(() => {
<VField>
<VControl>
<input
v-model="params.signDt"
:value="formatDate(completedPriceDataParams.regSdat)"
class="input custom-text-filter"
placeholder="계약체결일"
>
@@ -283,7 +361,7 @@ const contractPeriod = computed(() => {
<VField>
<VControl>
<input
v-model="params.contAmt"
v-model="completedPriceDataParams.amt"
class="input custom-text-filter"
placeholder="금액"
>
@@ -311,7 +389,7 @@ const contractPeriod = computed(() => {
<div>
<div>
<VDatePicker
v-model="params.regSdt"
v-model="completedPriceDataParams.regSdat"
color="green"
trim-weeks
>
@@ -333,10 +411,10 @@ const contractPeriod = computed(() => {
</div>
</td>
<td colspan="1">
<div class="pr-2">
<div class="">
<div>
<VDatePicker
v-model="params.regEdt"
v-model="completedPriceDataParams.regEdat"
color="green"
trim-weeks
>
@@ -364,20 +442,36 @@ const contractPeriod = computed(() => {
<tr>
<td>첨부파일</td>
<td colspan="1">
<VButton color="info">
<VButton color="info" @click="showFileInputs = !showFileInputs">
등록
</VButton>
</td>
<td colspan="5" />
<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
to="/app/DocumentManagement"
>
(임시)
</VButton>
<VButton> </VButton>
<VButton> </VButton>
</div>
@@ -389,11 +483,14 @@ const contractPeriod = computed(() => {
<style scoped lang="scss">
.table tbody td {
color: var(--smoke-white);
}
.datatable-table {
padding: 12px 12px;
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);
@@ -432,4 +529,16 @@ const contractPeriod = computed(() => {
.field {
margin-bottom: 0px;
}
.file-label {
cursor: pointer;
}
.file-input {
display: none;
}
.file-cta {
background: #eee;
padding: 4px 12px;
border-radius: 4px;
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import axios from 'axios'
import {useRouter} from "vue-router";
import {getContractList} from "/src/service/contract";
import regex, {formatCurrency} from "/@src/utils/common/regex.ts";
export type MinimalTheme = 'darker' | 'light'
@@ -21,57 +21,37 @@ const data = reactive({
priceData: [],
})
onMounted(async () => {
try {
const contractResponse = await axios.get('/api/cont/prcs')
data.contractData = Array.isArray(contractResponse.data) ? contractResponse.data : []
}
catch (error) {
console.log(error)
data.contractData = []
}
onBeforeMount(async () => {
await getContractListView()
})
const registerFormOpen = ref(false)
const isLoading = ref(false)
async function getContractListView() {
const paymentParams = {
params:{
title: '',
page: '1',
row: '10'
}
}
const result = await getContractList(paymentParams)
data.contractData = result
console.log('result', result)
}
watch(registerFormOpen, async (isOpen) => {
if (isOpen) {
isLoading.value = true
// error.value = null
try {
const priceResponse = await axios.get('/api/prcs/page?regSdt=1970-01-01&regEdt=2070-01-01&page=1&row=10')
console.log(priceResponse.data.content)
data.priceData = Array.isArray(priceResponse.data.content) ? priceResponse.data.content : []
}
catch (error) {
console.log(error)
data.priceData = []
}
}
})
const router = useRouter()
function handleRowClick(row) {
router.push({
name: 'DocumentManagement',
params: { id: row.contractDetailedData },
})
}
const params = reactive({
priceData: [],
flexColumn: [
{ key: 'cateNm', label: '분야' },
{ key: 'title', label: '제목' },
{ key: 'title', label: '계약명' },
{ key: 'compNm', label: '계약상대자' },
{ key: 'regNm', label: '담당자' },
{ key: 'regSdat', label: '최초등록일' },
{ key: 'stNm', label: '등록 상태' },
{ key: 'title', label: '비고' },
{ key: 'contAmt', label: '계약금액', format: formatCurrency },
{ key: 'contNo', label: '계약번호' },
{ key: 'signDt', label: '계약체결일' },
{ key: 'contStat', label: '계약상태' },
],
prcsParams: [],
})
</script>
<template>
@@ -111,7 +91,7 @@ const params = reactive({
</VLabel>
<VControl>
<input
v-model="params.title"
v-model="params.regNm"
class="input custom-text-filter"
placeholder="담당자명"
>
@@ -202,8 +182,9 @@ const params = reactive({
/>
</div>
</div>
<VButtons>
<VButtons class="is-right">
<VButton
class=""
color="primary"
icon="fas fa-plus"
elevated
@@ -213,43 +194,5 @@ const params = reactive({
등록
</VButton>
</VButtons>
<!-- 모달창 시작-->
<VModal
is="form"
:open="registerFormOpen"
title="계약관리 등록"
size="contract-big"
actions="right"
@submit.prevent="registerFormOpen = false"
@close="registerFormOpen = false"
>
<template #content>
<div class="modal-form">
<ComVFlexTable
:data="data.priceData"
:columns="{ cateNm: '분야',
title: '제목',
content: '담당자',
stNm: '등록상태',
stCd: '비고',
regSabun: '선택',
}"
:compact="true"
:separators="true"
@row-click="handleRowClick"
/>
</div>
</template>
<template #action>
<VButton
type="submit"
color="primary"
raised
>
Save Changes
</VButton>
</template>
</VModal>
</div>>
</template>

View File

@@ -3,17 +3,20 @@
import { getIntegratedApproval, updateIntegratedApproval } from '/src/service/integratedPayment'
import type { VFlexTableWrapperSortFunction, VFlexTableWrapperFilterFunction } from '/src/components/app-vuero/ComVFlexTableWrapper.vue'
import { users } from '/src/data/layouts/card-grid-v1'
import PriceDetail from "/@src/pages/app/priceDetail.vue";
onBeforeMount(async () => {
await getIntegratedPaymentList()
})
const isModalOpen = ref(false)
const selectedRow = ref<any>(null)
const prcsNo = ref('PRCS-20250521020')
const masks = ref({
modelValue: 'YYYY-MM-DD',
})
const params = reactive({
title: '',
priceData: [],
@@ -40,7 +43,7 @@ const params = reactive({
})
function formatRegDt(value) {
return value ? value.substring(0, 16) : ''
return value ? value.toString().replace(/[^0-9]/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',') : ''
}
async function getIntegratedPaymentList() {
@@ -67,8 +70,8 @@ async function updateIntegratedPaymentList() {
}
const result = await updateIntegratedApproval(paymentUpdateParams)
console.log(result)
alert('결재승인완료')
window.location.reload(true)
notyf.error("견적서가 없습니다.")
router.push('/app/paymentManagement')
}
const gubunMap = {
@@ -246,101 +249,14 @@ const onRowClick = (row: any) => {
<VModal
is="form"
v-model:open="isModalOpen"
title="결재"
size="big"
title="결재"
size="contract-big"
actions="center"
@submit.prevent="isModalOpen = false"
@close="isModalOpen = false"
>
<template #content>
<div class="modal-form">
<div class="columns is-multiline">
<div class="column is-4">
<div class="field">
<label>결재번호</label>
<div class="control">
<input
type="text"
class="input"
placeholder="결재번호"
:value="selectedRow?.apprNo"
readonly
>
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label>제목</label>
<div class="control">
<input
type="text"
class="input"
placeholder="제목"
:value="selectedRow?.title"
readonly
>
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label>작성자</label>
<div class="control">
<input
type="text"
class="input"
placeholder="작성자"
:value="selectedRow?.name"
readonly
>
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label>구분</label>
<div class="control">
<input
type="text"
class="input"
placeholder="구분"
:value="selectedRow?.process"
readonly
>
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label>결재상태</label>
<div class="control">
<input
type="text"
class="input"
placeholder="결재상태"
:value="selectedRow?.apprStat"
readonly
>
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label>등록일</label>
<div class="control">
<input
type="text"
class="input"
placeholder="등록일"
:value="selectedRow?.regDt"
readonly
>
</div>
</div>
</div>
</div>
</div>
<PriceDetail :prcsNo="prcsNo" />
</template>
<template #action>
<VButton type="submit" color="info" raised>반려</VButton>

View File

@@ -285,7 +285,7 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</span>
</td>
<td>제목</td>
<td colspan="3">
<td colspan="6">
<div class="column is-fullhd">
<VField class="pr-2">
<VControl>
@@ -301,13 +301,15 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</tr>
<tr>
<td>내용</td>
<td colspan="6">
<td colspan="10">
<div class="column is-fullhd">
<VField class="pr-2">
<VControl>
<textarea
v-model="generalParams.content"
class="input custom-text-filter"
rows="2"
style="height: 100px;"
placeholder="내용"
/>
</VControl>
@@ -337,12 +339,12 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
:clickable="true"
>
<template #body-cell="{ row, column, index, value }">
<div>
<div class="control" :class="column.key === 'actions' ? 'text-center' : ''">
<!-- 다른 editable 컬럼은 input -->
<input
v-if="column.editable"
v-model="row[column.key]"
class="editable-input"
class="input editable-input"
@blur="onInput(row, column)"
/>
<span v-else-if="column.key=='num'">{{index + 1}}</span>
@@ -395,11 +397,11 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
>
<template #body-cell="{ row, column, index, value }">
<!-- : 특정 컬럼이면 input, 아니면 그냥 출력 -->
<div>
<div class="control w-100" :class="column.key === 'delete'? 'text-center': ''">
<input
v-if="column.editable"
v-model="row[column.key]"
class="editable-input"
class="editable-input input"
/>
<span v-else-if="column.key=='num'">{{index+1}}</span>
<span v-else-if="column.key=='delete'" class="lnil lnil-close"
@@ -425,9 +427,9 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</VModal>
</td>
<td>등록기간</td>
<td colspan="5">
<td colspan="6">
<div class="columns">
<div class="column is-5">
<div class="column is-4">
<VDatePicker
v-model="generalParams.regSdat"
color="green"
@@ -448,8 +450,10 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</template>
</VDatePicker>
</div>
<div style="transform: translateY(15px)">~</div>
<div class="column is-5">
<div class="column is-1" style="text-align:center; line-height:2rem">
<span>~</span>
</div>
<div class="column is-4">
<VDatePicker
v-model="generalParams.regEdat"
color="green"
@@ -542,7 +546,7 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</div>
<div class="column is-12">
<VField class="pr-2">
<VLabel class="has-fullwidth">
<VLabel class="has-fullwidth" style="font-size: 1.3em;">
결재선
</VLabel>
</VField>
@@ -554,7 +558,7 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
:compact="true">
<template #body-cell="{ row, column, index, value }">
<!-- : 특정 컬럼이면 input, 아니면 그냥 출력 -->
<div>
<div class="w-100">
<span v-if="column.key=='gubunCd'" class="column">
<VField class="pr-1">
<VCodeSelect
@@ -596,7 +600,58 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</div>
</template>
<style scoped lang="scss">
<style lang="scss">
/*css 추가 start*/
.flex-table {
.flex-table-header {
padding: 0.1rem !important;
min-height: 40px;
border-top-left-radius: .75rem;
border-top-right-radius: .75rem;
}
.flex-table-cell {
.w-100 {
width: 100%;
}
}
.flex-table-item {
padding: 0.1rem !important;
min-height: 40px;
.column {
min-height: 40px;
padding: 0.1rem !important;
}
&:last-child {
border-bottom-left-radius: .75rem;
border-bottom-right-radius: .75rem;
}
}
}
.table-container {
overflow:hidden;
.control {
width: 100%;
}
td {
padding: 10px 10px !important;
.columns {
margin-left: 10px !important;
padding-top: 10px;
}
.column {
padding: 0 !important;
}
}
}
.text-center {
text-align:center;
}
/*css 추가 end*/
.table tbody td {
color: var(--smoke-white);
}

View File

@@ -0,0 +1,52 @@
import axios from 'axios'
/**
* 계약관리 조회(페이징)
* @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
* @property {string} params.cateNm - 분야
* @property {string} params.cateNm - 결제상태
* @property {string} params.regNm - 담당자
* @property {string} params.regSdat - 등록기간
* @returns
*/
export async function saveContract(params = {}) {
try {
const result = await axios.post(`/api/cont`,params)
return result
} 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;
}
}

View File

@@ -17,12 +17,6 @@ export async function getIntegratedApproval(params = {}) {
console.log(result)
if (result.status === 200) {
return result.data
} else if (result.status >= 500) {
throw new Error('서버 오류가 발생했습니다.')
} else if (result.status >= 400) {
throw new Error('잘못된 요청입니다.')
} else {
throw new Error(`예상치 못한 상태코드: ${result.status}`)
}
} catch (e) {
if (e.response) {
@@ -55,10 +49,6 @@ export async function updateIntegratedApproval(params = {}) {
})
if (result.status === 200) {
return result.data
} else if (result.status >= 500) {
throw new Error('서버 오류가 발생했습니다.')
} else {
throw new Error(`예상치 못한 상태코드: ${result.status}`)
}
}
catch (e) {

21
src/utils/common/regex.ts Normal file
View File

@@ -0,0 +1,21 @@
/**
* Resolve after `time` has been spend.
*
* @param time Time in milliseconds to wait
*/
const regex = {
bizNo: /^[0-9]{3}-[0-9]{2}-[0-9]{5}$/i,
email: /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i,
password: /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$/i,
currency: /\B(?=(\d{3})+(?!\d))/g,
}
// 천단위 콤마 포맷 함수 추가
export function formatCurrency(value) {
if (value === null || value === undefined || value === '') return ''
const num = Number(value.toString().replace(/[^0-9.]/g, ''))
return isNaN(num) ? '' : num.toLocaleString()
}
export default regex