세금계산서 리스트 및 상세

This commit is contained in:
focp212@naver.com
2025-09-25 14:01:03 +09:00
parent ba0b119e39
commit d5c12f4b69
34 changed files with 518 additions and 586 deletions

View File

@@ -0,0 +1,29 @@
import axios from 'axios';
import { API_URL_VAT_RETURN } from '@/shared/api/api-url-vat-return';
import { resultify } from '@/shared/lib/resultify';
import { CBDCAxiosError } from '@/shared/@types/error';
import {
VatReturnDetailParams,
VatReturnDetailResponse
} from '../model/types';
import {
useMutation,
UseMutationOptions
} from '@tanstack/react-query';
export const vatReturnDetail = (params: VatReturnDetailParams) => {
return resultify(
axios.post<VatReturnDetailResponse>(API_URL_VAT_RETURN.vatReturnDetail(), params),
);
};
export const useVatReturnDetailMutation = (options?: UseMutationOptions<VatReturnDetailResponse, CBDCAxiosError, VatReturnDetailParams>) => {
const mutation = useMutation<VatReturnDetailResponse, CBDCAxiosError, VatReturnDetailParams>({
...options,
mutationFn: (params: VatReturnDetailParams) => vatReturnDetail(params),
});
return {
...mutation,
};
};

View File

@@ -0,0 +1,29 @@
import axios from 'axios';
import { API_URL_VAT_RETURN } from '@/shared/api/api-url-vat-return';
import { resultify } from '@/shared/lib/resultify';
import { CBDCAxiosError } from '@/shared/@types/error';
import {
VatReturnListResponse,
VatReturnListParams,
} from '../model/types';
import {
useMutation,
UseMutationOptions
} from '@tanstack/react-query';
export const vatReturnList = (params: VatReturnListParams) => {
return resultify(
axios.post<VatReturnListResponse>(API_URL_VAT_RETURN.vatReturnList(), params),
);
};
export const useVatReturnListMutation = (options?: UseMutationOptions<VatReturnListResponse, CBDCAxiosError, VatReturnListParams>) => {
const mutation = useMutation<VatReturnListResponse, CBDCAxiosError, VatReturnListParams>({
...options,
mutationFn: (params: VatReturnListParams) => vatReturnList(params),
});
return {
...mutation,
};
};

View File

@@ -0,0 +1,81 @@
import { DefaulResponsePagination, DefaultRequestPagination } from '@/entities/common/model/types';
export enum VatReturnTabKeys {
List = 'List',
VatReference = 'VatReference',
};
export interface VatReturnTabProps {
activeTab: VatReturnTabKeys;
};
export enum VatReturnReceiptType {
ALL = 'ALL',
RECEIPT = 'RECEIPT',
BILL = 'BILL '
};
export enum VatReturnTargetType {
ALL = 'ALL',
GENERAL = 'GENERAL',
DIFFERENCE_COLLECTION = 'DIFFERENCE_COLLECTION',
REFUND_SETTLEMENT = 'REFUND_SETTLEMENT',
};
export interface VatReturnListParams {
mid: string;
startDate?: string;
endDate?: string;
receiptType?: VatReturnReceiptType;
targetType?: VatReturnTargetType;
pagination?: DefaultRequestPagination;
};
export interface VatReturnListResponse extends DefaulResponsePagination {
content: Array<VatReturnListContent>;
};
export interface VatReturnListContent {
id?: number
companyName?: string;
mid?: string;
issueDate?: string;
paymentMethod?: string;
amount?: number;
};
export interface ListDateGroupProps {
date?: string;
items?: Array<VatReturnListContent>
}
export interface ListItemProps {
id?: number;
companyName?: string;
mid?: string;
issueDate?: string;
paymentMethod?: string;
amount?: number;
}
export interface VatReturnDetailParams {
taxInvoiceNumber?: string;
};
export interface VatReturnDetailResponse {
id?: number;
totalAmount?: number;
supplyAmount?: number;
vatAmount?: number;
mid?: string;
targetBusinessStartDate?: string;
targetBusinessEndDate?: string;
issueDate?: string;
subject?: string;
targetType?: VatReturnTargetType;
receiptType?: VatReturnReceiptType;
email?: string;
receiverBusinessRegistrationNumber?: string;
receiverCompanyName?: string;
receiverCeoName?: string;
supplierBusinessRegistrationNumber?: string;
supplierCompanyName?: string;
supplierCeoName?: string;
};

View File

@@ -0,0 +1,46 @@
import moment from 'moment';
import 'moment/dist/locale/ko';
import { ListDateGroupProps } from '../model/types';
import { ListItem } from './list-item';
export const ListDateGroup = ({
date,
items
}: ListDateGroupProps) => {
moment.locale('ko');
const getStateDate = () => {
let stateDate = moment(date).format('YY.MM');
return stateDate;
};
const getListItem = () => {
let rs = [];
if(!!items && items.length>0){
for(let i=0;i<items.length;i++){
let key = 'ListItem-'+i;
rs.push(
<ListItem
key={ key }
id={ items[i]?.id || 0 }
companyName={ items[i]?.companyName || '' }
mid={ items[i]?.mid || '' }
issueDate={ items[i]?.issueDate || '' }
paymentMethod={ items[i]?.paymentMethod || '' }
amount={ items[i]?.amount || 0 }
></ListItem>
)
}
}
return rs;
};
return (
<>
<div className="date-group">
<div className="date-header">{ getStateDate() }</div>
{ getListItem() }
</div>
</>
);
};

View File

@@ -0,0 +1,166 @@
import { motion } from 'framer-motion';
import { IMAGE_ROOT } from '@/shared/constants/common';
import { VatReturnReceiptType, VatReturnTargetType } from '../model/types';
import { FilterMotionDuration, FilterMotionStyle, FilterMotionVariants } from '@/entities/common/model/constant';
export interface ListFilterProps {
filterOn: boolean;
setFilterOn: (filterOn: boolean) => void;
mid: string;
startDate: string;
endDate: string;
receiptType: VatReturnReceiptType;
targetType: VatReturnTargetType;
setMid: (mid: string) => void;
setStartDate: (date: string) => void;
setEndDate: (date: string) => void;
setReceiptType: (receiptType: VatReturnReceiptType) => void;
setTargetType: (targetType: VatReturnTargetType) => void;
};
export const ListFilter = ({
filterOn,
setFilterOn,
mid,
startDate,
endDate,
receiptType,
targetType,
setMid,
setStartDate,
setEndDate,
setReceiptType,
setTargetType
}: ListFilterProps) => {
const variants = {
hidden: { x: '100%' },
visible: { x: '0%' },
};
const onClickToClose = () => {
setFilterOn(false);
};
return (
<>
<motion.div
id="fullMenuModal"
className="full-menu-modal"
initial="hidden"
animate={ (filterOn)? 'visible': 'hidden' }
variants={ FilterMotionVariants }
transition={ FilterMotionDuration }
style={ FilterMotionStyle }
>
<div className="full-menu-container">
<div className="full-menu-header">
<div className="full-menu-title center"></div>
<div className="full-menu-actions">
<button
className="full-menu-close"
onClick={ () => onClickToClose() }
>
<img
src={ IMAGE_ROOT + '/ico_close.svg' }
alt="닫기"
/>
</button>
</div>
</div>
<div className="option-list pb-120">
<div className="opt-field">
<div className="opt-label"></div>
<div className="opt-controls">
<select className="flex-1">
<option>nictest001m</option>
</select>
</div>
</div>
<div className="opt-field mt_top">
<div className="opt-label"></div>
<div className="opt-controls col below h36">
<div className="chip-row">
<span className="keyword-tag active"></span>
<span className="keyword-tag">2</span>
<span className="keyword-tag">3</span>
<span className="keyword-tag"></span>
</div>
<div className="range-row">
<div className="input-wrapper date">
<input
className="date-input"
type="text"
placeholder="날짜 선택"
value="2025.06.08"
readOnly={ true }
/>
<button
className="date-btn"
type="button"
>
<img
src={ IMAGE_ROOT + '/ico_date.svg' }
alt="날짜 선택"
/>
</button>
</div>
<span className="beetween">~</span>
<div className="input-wrapper date">
<input
className="date-input"
type="text"
placeholder="날짜 선택"
value="2025.06.08"
readOnly={ true }
/>
<button
className="date-btn"
type="button"
>
<img
src={ IMAGE_ROOT + '/ico_date.svg' }
alt="날짜 선택"
/>
</button>
</div>
</div>
</div>
</div>
<div className="opt-field">
<div className="opt-label"> </div>
<div className="opt-controls col below h36">
<div className="chip-row">
<span className="keyword-tag active"></span>
<span className="keyword-tag"></span>
<span className="keyword-tag"></span>
<span
className="keyword-tag"
style={{ visibility: 'hidden' }}
></span>
</div>
</div>
</div>
<div className="opt-field">
<div className="opt-label"> </div>
<div className="opt-controls col below h36">
<div className="chip-row">
<span className="keyword-tag active"></span>
<span className="keyword-tag"></span>
<span className="keyword-tag"></span>
<span className="keyword-tag"></span>
</div>
</div>
</div>
</div>
<div className="apply-row">
<button className="btn-50 btn-blue flex-1"></button>
</div>
</div>
</motion.div>
</>
);
};

View File

@@ -0,0 +1,53 @@
import { NumericFormat } from 'react-number-format';
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { ListItemProps } from '../model/types';
import moment from 'moment';
export const ListItem = ({
id,
companyName,
mid,
issueDate,
paymentMethod,
amount
}: ListItemProps) => {
const { navigate } = useNavigate();
const onClickToNavigate = () => {
navigate(PATHS.vatReturn.detail, {
state: {
taxInvoiceNumber: id
}
});
};
return (
<>
<div
className="transaction-item approved"
onClick={ () => onClickToNavigate() }
>
<div className="transaction-status">
<div className="status-dot blue"></div>
</div>
<div className="transaction-content">
<div className="transaction-title">{ companyName } ({ mid })</div>
<div className="transaction-details">
<span>{ moment(issueDate).format('YYYY.MM.DD') }</span>
<span className="separator">|</span>
<span>{ paymentMethod }</span>
</div>
</div>
<div className="transaction-amount">
<NumericFormat
value={ amount }
thousandSeparator
displayType="text"
suffix='원'
></NumericFormat>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,144 @@
import moment from 'moment';
import { useEffect, useState } from 'react';
import { IMAGE_ROOT } from '@/shared/constants/common';
import { ListFilter } from './list-filter';
import { SortOptionsBox } from '@/entities/common/ui/sort-options-box';
import { SortByKeys } from '@/entities/common/model/types';
import { DEFAULT_PAGE_PARAM } from '@/entities/common/model/constant';
import {
VatReturnListContent,
VatReturnListParams,
VatReturnReceiptType,
VatReturnTargetType
} from '../model/types';
import { useVatReturnListMutation } from '../api/use-vat-return-list-mutation';
import { ListDateGroup } from './list-date-group';
export const ListWrap = () => {
const [filterOn, setFilterOn] = useState<boolean>(false);
const [sortBy, setSortBy] = useState<SortByKeys>(SortByKeys.New);
const [listItems, setListItems] = useState<Record<string, Array<VatReturnListContent>>>({});
const [pageParam, setPageParam] = useState(DEFAULT_PAGE_PARAM);
const [mid, setMid] = useState<string>('nictest00m');
const [startDate, setStartDate] = useState(moment().subtract(1, 'month').format('YYYY-MM-DD'));
const [endDate, setEndDate] = useState(moment().format('YYYY-MM-DD'));
const [receiptType, setReceiptType] = useState<VatReturnReceiptType>(VatReturnReceiptType.ALL);
const [targetType, setTargetType] = useState<VatReturnTargetType>(VatReturnTargetType.ALL);
const { mutateAsync: vatReturnList } = useVatReturnListMutation();
const callList = () => {
let params: VatReturnListParams = {
mid: mid,
startDate: startDate,
endDate: endDate,
receiptType: receiptType,
targetType: targetType,
};
vatReturnList(params).then((rs) => {
setListItems(assembleData(rs.content));
});
};
const onClickToOpenFIlter = () => {
setFilterOn(true);
};
const onClickToSort = (sort: SortByKeys) => {
setSortBy(sort);
};
useEffect(() => {
callList();
}, []);
const getListDateGroup = () => {
let rs = [];
if(Object.keys(listItems).length > 0){
for (const [key, value] of Object.entries(listItems)) {
rs.push(
<ListDateGroup
key={ key }
date={ key }
items={ value }
></ListDateGroup>
);
}
}
return rs;
};
const assembleData = (content: Array<VatReturnListContent>) => {
let data: any = {};
if(content && content.length > 0){
for(let i=0;i<content?.length;i++){
let groupDate = moment(content[i]?.issueDate).format('YYYYMMDD');
if(!!groupDate && !data.hasOwnProperty(groupDate)){
data[groupDate] = [];
}
if(!!groupDate && data.hasOwnProperty(groupDate)){
data[groupDate].push(content[i]);
}
}
}
return data;
};
return (
<>
<div className="summary-section pt-30">
<div className="credit-controls">
<div>
<input
type="text"
className="credit-period"
value={moment(startDate).format('YYYY.MM.DD') + '-' + moment(endDate).format('YYYY.MM.DD')}
readOnly={ true }
/>
<button
className="filter-btn"
onClick={ () => onClickToOpenFIlter() }
>
<img
src={ IMAGE_ROOT + '/ico_setting.svg' }
alt="검색옵션"
/>
</button>
</div>
<button className="download-btn">
<img
src={ IMAGE_ROOT + '/ico_download.svg' }
alt="다운로드"
/>
</button>
</div>
</div>
<div className="filter-section mt-10">
<SortOptionsBox
sortBy={ sortBy }
onClickToSort={ onClickToSort }
></SortOptionsBox>
</div>
<div className="transaction-list">
{ getListDateGroup() }
</div>
<div className="apply-row bottom-padding">
<button className="btn-50 btn-blue flex-1">수기발행</button>
</div>
<ListFilter
filterOn={ filterOn }
setFilterOn={ setFilterOn }
mid={ mid }
startDate={ startDate }
endDate={ endDate }
receiptType={ receiptType }
targetType={ targetType }
setMid={ setMid }
setStartDate={ setStartDate }
setEndDate={ setEndDate }
setReceiptType={ setReceiptType }
setTargetType={ setTargetType }
></ListFilter>
</>
);
};

View File

@@ -0,0 +1,85 @@
import moment from 'moment';
import { VatReturnDetailResponse } from '../../model/types';
import { SectionTitleArrow } from '@/entities/common/ui/section-title-arrow';
import { NumericFormat } from 'react-number-format';
import SlideDown from 'react-slidedown';
import 'react-slidedown/lib/slidedown.css';
import { useState } from 'react';
export interface AmountSectionProps {
detail: VatReturnDetailResponse;
};
export const AmountSection = ({
detail
}: AmountSectionProps) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const openSection = () => {
const status = !isOpen;
setIsOpen(status);
};
return (
<>
<div className="txn-num-group">
<div className="txn-amount">
<div className="value">
<NumericFormat
value={ detail.totalAmount }
thousandSeparator
displayType="text"
></NumericFormat>
<span className="unit"></span>
</div>
<button
className="chip-btn"
type="button"
onClick={ () => openSection() }
>
<span></span> <SectionTitleArrow isOpen={ isOpen }></SectionTitleArrow>
</button>
</div>
<SlideDown className={'my-dropdown-slidedown'}>
{ isOpen &&
<div className="amount-expand">
<ul className="amount-list">
<li className="amount-item">
<span className="label">·&nbsp;&nbsp;</span>
<span className="value">
<NumericFormat
value={ detail.supplyAmount }
thousandSeparator
displayType="text"
suffix='원'
></NumericFormat>
</span>
</li>
<li className="amount-item">
<span className="label">·&nbsp;&nbsp;VAT</span>
<span className="value">
<NumericFormat
value={ detail.vatAmount }
thousandSeparator
displayType="text"
suffix='원'
></NumericFormat>
</span>
</li>
</ul>
</div>
}
</SlideDown>
<div className="txn-mid">
<span className="value">{ detail.mid }</span>
</div>
<div className="txn-mid">
<span className="value">{ moment(detail.issueDate).format('YYYY.MM.DD') }</span>
</div>
<div className="txn-doc">
<button className="doc-btn" type="button"></button>
<button className="doc-btn" type="button"></button>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,49 @@
import moment from 'moment';
import { VatReturnDetailResponse } from '../../model/types';
export interface IssueSectionProps {
detail: VatReturnDetailResponse
};
export const IssueSection = ({
detail
}: IssueSectionProps) => {
return (
<>
<div className="txn-section">
<div className="section-title"> </div>
<ul className="kv-list">
<li className="kv-row">
<span className="k">MID</span>
<span className="v">{ detail.mid }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ moment(detail.targetBusinessStartDate).format('YYYY.MM.DD') } ~ { moment(detail.targetBusinessEndDate).format('YYYY.MM.DD') }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ moment(detail.issueDate).format('YYYY.MM.DD') }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.subject }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.targetType }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.receiptType }</span>
</li>
<li className="kv-row">
<span className="k">Email</span>
<span className="v">{ detail.email }</span>
</li>
</ul>
</div>
</>
);
};

View File

@@ -0,0 +1,32 @@
import { VatReturnDetailResponse } from '../../model/types';
export interface ReceiverSectionProps {
detail: VatReturnDetailResponse
};
export const ReceiverSection = ({
detail
}: ReceiverSectionProps) => {
return (
<>
<div className="txn-section">
<div className="section-title"> </div>
</div>
<ul className="kv-list">
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.receiverBusinessRegistrationNumber }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.receiverCompanyName }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.receiverCeoName }</span>
</li>
</ul>
</>
);
};

View File

@@ -0,0 +1,32 @@
import { VatReturnDetailResponse } from '../../model/types';
export interface SupplierSectionProps {
detail: VatReturnDetailResponse
};
export const SupplierSection = ({
detail
}: SupplierSectionProps) => {
return (
<>
<div className="txn-section">
<div className="section-title"> </div>
</div>
<ul className="kv-list">
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.supplierBusinessRegistrationNumber }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.supplierCompanyName }</span>
</li>
<li className="kv-row">
<span className="k"></span>
<span className="v">{ detail.supplierCeoName }</span>
</li>
</ul>
</>
);
};

View File

@@ -0,0 +1,26 @@
export const VatReferenceReqFail = () => {
return (
<>
<div className="success-page">
<div className="success-body">
<div className="error-icon" aria-hidden="true"></div>
<h1 className="success-title">
<span> </span>
<br/>
<span> </span>
</h1>
<div className="success-result">
<p className="result-text align-left position_label">
<span> :</span>
<span>[9997] .<br/>NICEPAY로 .</span>
</p>
</div>
</div>
<div className="apply-row">
<button className="btn-50 btn-blue flex-1"></button>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,29 @@
export const VatReferenceReqSuccess = () => {
return (
<>
<div className="success-page">
<div className="success-body">
<div className="success-icon" aria-hidden="true"></div>
<h1 className="success-title">
<span> </span>
<br/>
<span> .</span>
</h1>
<p className="success-subtitle"> .</p>
<div className="success-result">
<p className="result-text">
<span> :</span> <span>2025.06.04</span>
<br/>
<span> :</span> <span>TEST@NICEPAY.CO.KR</span>
</p>
</div>
<p className="success-note dot"> <br/> .</p>
</div>
<div className="apply-row">
<button className="btn-50 btn-blue flex-1"></button>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,8 @@
export const VatReferenceWrap = () => {
return (
<>
</>
)
};

View File

@@ -0,0 +1,37 @@
import { PATHS } from '@/shared/constants/paths';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import {
VatReturnTabKeys,
VatReturnTabProps
} from '../model/types';
export const VatReturnTab = ({
activeTab
}: VatReturnTabProps) => {
const { navigate } = useNavigate();
const onClickToNavigation = (tab: VatReturnTabKeys) => {
if(activeTab !== tab){
if(tab === VatReturnTabKeys.List){
navigate(PATHS.vatReturn.list);
}
else if(tab === VatReturnTabKeys.VatReference){
navigate(PATHS.vatReturn.vatReference);
}
}
};
return(
<>
<div className="subTab">
<button
className={`subtab-btn ${(activeTab === VatReturnTabKeys.List)? 'active': ''}` }
onClick={ () => onClickToNavigation(VatReturnTabKeys.List) }
> </button>
<button
className={`subtab-btn ${(activeTab === VatReturnTabKeys.VatReference)? 'active': ''}` }
onClick={ () => onClickToNavigation(VatReturnTabKeys.VatReference) }
> </button>
</div>
</>
);
};