웹 개발/IBSheet8

[IBSheet] Excel 내보내기

cha430 2025. 6. 24. 14:17

 

IBSheet

 

Events: { 내에 on~이벤트 작성

 

onExportFinish: function (evtParam) {
    console.log(evtParam.eventName + "엑셀 내보내기 실행");
},

 

 

onMunted { 외부에 함수 작성

// excel 내보내기
function excelExport() {
    if (sheet1) {
        sheet1.exportData({
            fileName: "AssetsList.xlsx",
            fileType: "xlsx",
            sheetDesign: 1,
            exceptCol: ["acqDelete"] // 삭제 버튼 컬럼은 제외
        });
        console.log("엑셀 내보내기 완료");
    } else {
        openAlertDialog("엑셀 내보내기를 할 수 없습니다.");
    }
}

 

 

 

제외 전체 Vue 코드 

<template>
    <v-app class="v-application">
        <v-app-bar :elevation="2" style="background-color: whitesmoke" height="60">
            <v-app-bar-nav-icon class="ml-2" @click="sideBarClicked = !sideBarClicked"/>
            <div class="header-logo" >
                <v-img class="header-img" src="/kloz.png" alt="KLOZ 로고" />
            </div>

            <!-- 우측 상단 -->
            <div class="mr-8"><span v-if="userId">{{userId}}</span></div>
            <div><v-icon @click="logout" class="v-icon-img">mdi-logout</v-icon></div>

        </v-app-bar>
        <div class="content-container">
            <!-- 왼쪽 사이드바 -->
            <div class="sidebar" @mouseenter="sideBarHovered = true" @mouseleave="sideBarHovered = false"
                 :class="{ open: isSideBarOpen }">
                <div v-if="isSideBarOpen" class="date-time sidebar-img">
                    <div class="menu-userInfo">{{displayName}}</div>
                    <div class="menu-userInfo">{{position}}</div>
                    <div style="margin-top: 20px;">{{ today }}</div>
                    <div>{{ nowTime }}</div>
                </div>
                <div class="menu-item" @click="">
                    <v-icon class="v-icon-img">mdi-home</v-icon>
                    <span v-if="isSideBarOpen">홈</span>
                </div>
                <div class="menu-item" @click="goToAsset">
                    <v-icon class="v-icon-img">mdi-office-building</v-icon>
                    <span v-if="isSideBarOpen">고정자산</span>
                </div>
                <div class="menu-item">
                    <v-icon class="v-icon-img">mdi-file-document</v-icon>
                    <span v-if="isSideBarOpen">메뉴2</span>
                </div>
            </div>

            <div class="main" :class="{ shift: sideBarClicked }">
                <div variant="h6" class="ml-2 font-weight-bold">자산관리</div>
                <div class="dropdown-container">
                    <div class="left-controls">
                        <v-select
                            v-model="selectedSubject"
                            :items="selectTitleOptions"
                            label="계정과목"
                            variant="outlined"
                            density="compact"
                            @update:modelValue="searchAsset"
                        ></v-select>
                        <v-text-field
                            v-model="kwd"
                            label="자산명"
                            @keydown.enter="searchAsset"
                            clearable
                            variant="outlined" density="compact" icon>
                        </v-text-field>
                        <VueDatePicker
                            v-model="dateRange"
                            range
                            :partial-range="false"
                            :enable-time-picker="false"
                            auto-apply
                            clearable
                            placeholder="취득일자"
                            format="yyyy-MM-dd"
                            @update:model-value="searchAsset"
                            style="max-width: 270px;"
                        />
                        <v-btn @click="searchAsset" color="info" rounded="lg" class="btn">검색
                            <v-icon icon="mdi-magnify" style="margin-left: 10px;"/>
                        </v-btn>
                    </div>
                    <v-btn @click="doOpenPDF"> PDF </v-btn>
                    <v-btn @click="doOpenExcel"> EXCEL </v-btn>

                </div>
                <div class="insertBtn">
                    <v-btn @click="openDialog" color="success" rounded="lg" class="btn">등록
                        <v-icon icon="mdi-pencil" style="margin-left: 10px;"/>
                    </v-btn>
                </div>
                <!-- IBSheet -->
                <div class="klozSheet" id="klozSheet" ></div>
            </div>
        </div>
    </v-app>


    <!-- 등록 모달 -->

    <v-dialog v-model="dialog" width="600px">
        <v-card class="v-card" style="background-color: whitesmoke">
            <v-card-title class="v-card-title">{{ editAssetCode ? '수정' : '등록' }}</v-card-title>

            <v-card-text>
                <v-row>
                    <v-col cols="6">
                        <v-select label="계정과목*" v-model="titleModal" :items="selectTitleOptions" variant="outlined"
                                  density="compact" hide-details></v-select>
                    </v-col>
                    <v-col cols="6">
                        <v-text-field label="자산명*" v-model="nameModal" variant="outlined"
                                      density="compact" hide-details></v-text-field>
                    </v-col>
                </v-row>

                <v-row>
                    <v-col cols="4">
                        <v-text-field label="수량*" v-model="quantityModal" variant="outlined"
                                      density="compact" hide-details></v-text-field>
                    </v-col>
                    <v-col cols="4">
                        <v-text-field label="취득일*" v-model="regDateModal" type="date"
                                      variant="outlined" density="compact" hide-details></v-text-field>
                    </v-col>
                    <v-col cols="4">
                        <v-text-field label="취득금액*" v-model="priceModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                </v-row>

                <hr class="middle-line">

                <v-row>
                    <v-col cols="4">
                        <v-text-field label="프로젝트" v-model="projectModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                    <v-col cols="4">
                        <v-text-field label="사용부서" v-model="deptModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                    <v-col cols="4">
                        <v-text-field label="사용자" v-model="assetUserIdModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                </v-row>

                <v-row>
                    <v-col cols="4">
                        <v-text-field label="매입처" v-model="vendorModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                    <v-col cols="4">
                        <v-text-field label="규격" v-model="specModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                    <v-col cols="4">
                        <v-text-field label="모델" v-model="modelModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                </v-row>

                <v-row>
                    <v-col cols="12">
                        <v-text-field label="비고" v-model="remarkModal" variant="outlined" density="compact"
                                      hide-details></v-text-field>
                    </v-col>
                </v-row>
                <h4 style="font-size:18px; font-weight:500; margin: 20px 0 0 10px;">* 은 필수 값 입니다.</h4>
            </v-card-text>

            <v-card-actions class="justify-end">
                <v-btn @click="cancelDialog">취소</v-btn>
                <v-btn color="primary" @click="dialogSave">{{ editAssetCode ? '수정 완료' : '등록 완료' }}</v-btn>
            </v-card-actions>
        </v-card>
    </v-dialog>

    <v-dialog v-model="alertDialog" max-width="400">
        <v-card>
            <v-card-title class="headline">알림</v-card-title>
            <v-card-text>{{ alertMessage }}</v-card-text>
            <v-card-actions>
                <v-spacer></v-spacer>
                <v-btn color="primary" @click="alertDialog = false">확인</v-btn>
            </v-card-actions>
        </v-card>
    </v-dialog>

</template>

<script setup>
import {ref, onMounted, onUnmounted, computed} from 'vue'
import {doRequest} from "@/assets/common/js/common.js";
import loader from '@ibsheet/loader'

import axios from 'axios'
import dayjs from 'dayjs'
import VueDatePicker from '@vuepic/vue-datepicker'
import '@vuepic/vue-datepicker/dist/main.css';
import {isProduct} from "@/assets/common/js/common.js";

const today = ref('')
const nowTime = ref('')
let timer = null

const alertDialog = ref(false);
const alertMessage = ref('');

let sheet1 = null;

const dateRange = ref([]);    // 날짜 검색
const kwd = ref('') // 자산명 검색

const userId = ref('')  // 사용자 아이디
const displayName = ref('') // 사용자 이름
const position = ref('')    // 직급

const editAssetCode = ref(null);

const sideBarHovered = ref(false)
const sideBarClicked = ref(false)

const isSideBarOpen = computed(() => sideBarClicked.value || sideBarHovered.value)

// 사이드바에서 고정자산 클릭 시
function goToAsset() {
    window.location.reload();
}

// dialog
const dialog = ref(false)
const titleModal = ref('')
const nameModal = ref('')
const quantityModal = ref('')
const regDateModal = ref('')
const priceModal = ref('')
const projectModal = ref('')
const deptModal = ref('')
const assetUserIdModal = ref('')
const vendorModal = ref('')
const specModal = ref('')
const modelModal = ref('')
const remarkModal = ref('')

const resetDialog = () => {
    titleModal.value = '';
    nameModal.value = '';
    quantityModal.value = '';
    regDateModal.value = '';
    priceModal.value = '';
    projectModal.value = '';
    deptModal.value = '';
    assetUserIdModal.value = '';
    vendorModal.value = '';
    specModal.value = '';
    modelModal.value = '';
    remarkModal.value = '';
    editAssetCode.value = null;
};

const openAlertDialog = (message) => {
    alertMessage.value = message;
    alertDialog.value = true;
};

const openDialog = () => {
    dialog.value = true;
    resetDialog();
    setTimeout(() => {
        document.activeElement?.blur();
    }, 100);
}

const cancelDialog = () => dialog.value = false


const openEditDialog = (rowData) => {
    titleModal.value = rowData.accountTitle;
    nameModal.value = rowData.assetName;
    quantityModal.value = rowData.acqQty;
    regDateModal.value = rowData.acqDate ? dayjs(rowData.acqDate).format('YYYY-MM-DD') : new Date();
    priceModal.value = rowData.acqPrice;
    projectModal.value = rowData.project || '';
    deptModal.value = rowData.useDept || '';
    assetUserIdModal.value = rowData.assetUserId || '';
    vendorModal.value = rowData.vendor || '';
    specModal.value = rowData.spec || '';
    modelModal.value = rowData.model || '';
    remarkModal.value = rowData.remark || '';

    editAssetCode.value = rowData.assetCode;

    dialog.value = true;
};

const selectTitleOptions = ref([])
const selectedSubject = ref('')

// 계정과목 불러오기
const loadAccountTitles = async () => {
    try {
        const res = await axios.get('/main/loadAccountTitles.html')
        const list = res.data

        selectTitleOptions.value = list.map(item => item.accountTitle)

    } catch (err) {
        console.error('계정과목 불러오기 실패', err)
    }
}

const dialogSave = async () => {
    const modalData = {
        accountTitle: titleModal.value,
        assetName: nameModal.value,
        acqQty: quantityModal.value,
        acqDate: regDateModal.value ? dayjs(regDateModal.value).format('YYYY-MM-DD') : null,
        acqPrice: priceModal.value,
        project: projectModal.value || null,
        assetUserId: assetUserIdModal.value || null,
        useDept: deptModal.value || null,
        vendor: vendorModal.value || null,
        spec: specModal.value || null,
        model: modelModal.value || null,
        remark: remarkModal.value || null
    }

    let url = '';
    let method = '';

    if (!titleModal.value || !nameModal.value || !quantityModal.value || !regDateModal.value || !priceModal.value) {
        openAlertDialog('필수 항목을 모두 입력해 주세요.');
        return;
    }

    if (editAssetCode.value) {
        url = '/main/updateAsset.do';
        method = 'put';
        modalData.assetCode = editAssetCode.value;
    } else {
        url = '/main/insertAsset.do';
        method = "post";
    }

    try {
        const res = await axios({method: method, url: url, data: modalData});

        if (res.data && res.data.result === 1) {
            openAlertDialog(editAssetCode.value ? "수정되었습니다." : "등록되었습니다.");
            dialog.value = false;
            resetDialog();

        } else {
            // 서버에서 보낸 에러 메시지가 있다면 사용, 없다면 일반 메시지
            openAlertDialog(res.data && res.data.message ? res.data.message : "등록/수정 실패");
        }
    } catch (error) {
        console.error(editAssetCode.value ? "수정 실패하였습니다." : "등록 실패하였습니다.", error);

        const errorMessage = error.response && error.response.data && error.response.data.message
            ? error.response.data.message : '네트워크 오류 또는 서버 응답 문제';
        openAlertDialog(`작업 중 오류 발생: ${errorMessage}`);
    }
};


// 현재 시간
function updateTime() {
    const now = new Date()
    nowTime.value = now.toLocaleTimeString('ko-KR', {
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
        hour12: false
    })
}

onMounted(async () => {
    // 계정과목 데이터 로드
    await loadAccountTitles();

    // IBSheet 로더 설정
    loader.config({
        registry: [{
            name: 'ibsheet',
            baseUrl: isProduct? '../sheet' : '/sheet',
            theme: 'mint',
            locales: ['ko', 'en'],
            plugins: ['common', 'dialog', 'excel']
        }]
    });
    loader.load();


    // IBSheet 초기화 옵션 정의
    const initOptions = {
        Cfg: {
            DebugOption:"error",
            SearchMode: 3,
            CustomScroll: 0,
            HeaderCheck: 1,
            UseButton: 1,
            FilterDefaultsIconLeft: true,
            "Alternate": 2,     // IBSheet 행 배경색
            // InfoRowConfig: {Visible: false},
            FitWidth: false,
            IgnoreFocused: 1
        },
        Cols: [
            { Header: "계정과목", Name: "accountTitle", Type: "Text", Align: "Center", CanEdit: 3, RelWidth: 3 },
            { Header: "자산코드", Name: "assetCode", Type: "Text", Align: "Center", CanEdit: 3, RelWidth: 5 },
            { Header: "자산명", Name: "assetName", Type: "Text", Align: "Center", CanEdit: 1, RelWidth: 5 },
            { Header: "취득수량 (개)", Name: "acqQty", Type: "Float", Align: "right", Format: "#,###.00", CanEdit: 1, RelWidth: 3 },
            { Header: "취득일자", Name: "acqDate", Type: "Date", Align: "Center", Format: "yyyy-MM-dd", CanEdit: 1, RelWidth: 5 },
            { Header: "취득금액 (원)", Name: "acqPrice", Type: "Float", Format: "#,###.00", CanEdit: 1, RelWidth: 3 },
            { Header: "삭제", Name: "acqDelete", Type: "Button", ButtonText: "❌", RelWidth:1}
        ],
        Events: {
            onDataLoad: function (evtParam) {
                console.log(evtParam.eventName + "데이터 로드 실행");
            },
            onRenderFirstFinish: function (evtParam) {
                console.log(evtParam.eventName + "시트 처음 렌더링 실행");
                const sheet = evtParam.sheet;

                sheet.doSearchPaging({
                    url: "/main/searchPaging.do",
                    method: "POST",
                    reqHeader: {"Content-Type": "application/json"},
                    param: {
                        kwd: kwd.value,
                        accountTitle: selectedSubject.value,
                        startDate: dateRange.value?.[0],
                        endDate: dateRange.value?.[1]
                    },
                });
            },
            onSearchFinish: function (evtParam) {
                console.log(evtParam.eventName + "검색 및 페이징 실행");
            },
            onAfterChange:  function (evtParam) {
                console.log(evtParam.eventName + ' 단일 셀 수정 실행');

                const rowData = evtParam.row;
                const colName = evtParam.col;
                const newValue = evtParam.val;
                const oldValue = rowData[colName + 'BeforeVal'] ?? rowData[colName + 'Orig'] ?? null;

                if (newValue === oldValue) {
                    console.log("단일 셀 수정 : 값이 변경되지 않아 서버 요청을 보내지 않음");
                    return;
                }

                const changedData = {
                    assetCode: rowData.assetCode,
                    col: colName,
                    value: newValue
                };

                console.log("단일 셀 수정 시 서버로 전송하는 데이터:", JSON.stringify(changedData));

                try {
                    const res = axios.put(`/main/updateColAsset.do`, changedData);
                    if (res.data && res.data.result === 1) {
                    } else {
                        openAlertDialog(res.data.message || "단일 셀 수정 실패");
                    }
                } catch (err) {
                    console.error("단일 셀 수정 중 오류:", err);
                }
            },
            onAfterClick: function (evtParam) {
                if (evtParam.col === 'assetCode') {
                    const rowData = evtParam.row;
                    openEditDialog(rowData);
                }else if(evtParam.col === 'acqDelete') {

                }
            },
            onClick: function (evtParam) {
                if (evtParam.col === 'acqDelete') {
                    const rowData = evtParam.row;
                    const formattedPrice = new Intl.NumberFormat('ko-KR', {
                        minimumFractionDigits: 0,
                        maximumFractionDigits: 0
                    }).format(rowData.acqPrice);

                    if (confirm(`정말 삭제하시겠습니까?\n\n계정과목 : ${rowData.accountTitle}\n자 산 명  : ${rowData.assetName}\n취득금액 : ${formattedPrice}원`)) {
                        try {
                            const res = axios.delete(`/main/deleteAsset.do?assetCode=${rowData.assetCode}`);
                            if (res.data && res.data.result === 1) {
                                openAlertDialog('삭제 되었습니다.');
                                evtParam.sheet.deleteRow(rowData);

                            } else {
                                openAlertDialog(res.data.message || '삭제에 실패하였습니다.');
                            }
                        } catch (error) {
                            console.error('삭제 중 오류 발생', error);
                            const errorMessage = error.response && error.response.data && error.response.data.message
                                ? error.response.data.message : '네트워크 오류 또는 서버 응답 문제';
                            openAlertDialog(`삭제 중 오류가 발생했습니다: ${errorMessage}`);
                        }
                    }
                }
            },
        }
    };

    try {
        // IBSheet 인스턴스 생성
        loader.createSheet({
            id: 'klozSheet',
            el: 'klozSheet',
            options: initOptions
        }).then(sheet => {
            // 주의: 해당 구간에서 데이터 조회를 하면 안됩니다. 데이터 조회는 onRenderFirstFinish 이벤트에서 실행해야합니다.
            // 생성된 시트객체를 페이지 공통 객체에 넣어두고 사용
            sheet1 = sheet;
        });
        console.log("시트 생성 완료")

    } catch (err) {
        console.error("IBSheet 생성 실패", err);
        openAlertDialog('데이터 로드에 실패했습니다. 시스템 관리자에게 문의하세요.');
    }

    // 로그인
    try {
        const response = await axios.get('/login/getUserId.do', {
            withCredentials: true
        });
        if(response.data.success) {
            userId.value = response.data.userId;
            displayName.value = response.data.displayName;
            position.value = response.data.position;
            console.log('로그인 유저 정보 조회 완료');
        } else {
            console.log('로그인 유저 정보 조회 실패');
            window.location.href = '/login/login.html';
        }
    } catch (error) {
        console.error('로그인 유저 정보 조회 중 오류 발생:', error);
        window.location.href = '/login/login.html';
    }

    // 현재 시간
    const now = new Date();
    today.value = now.toLocaleDateString('ko-KR', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        weekday: 'short'
    })
    updateTime();
    timer = setInterval(updateTime, 1000);

});

onUnmounted(() => {
    loader.removeSheet('klozSheet'); // 새로고침 시 동일ID Sheet 호출 방어

    clearInterval(timer);
    function updateTime() {
        const now = new Date()
        nowTime.value = now.toLocaleTimeString('ko-KR', {
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        })
    }
    return {today, nowTime}
})


function doOpenPDF() {
    location.href = '../report/reportPDF.do'
}

function doOpenExcel() {
    location.href= '../report/reportExcel.do'
}

// 검색
const searchAsset = async () => {
    console.log("searchAsset 실행 시작");

    const params = {
        kwd: kwd.value,
        accountTitle: selectedSubject.value,
        startDate: dateRange.value?.[0],
        endDate: dateRange.value?.[1],
    };

        sheet1.doSearchPaging({
            url: "/main/searchPaging.do",
            method: "POST",
            reqHeader: {"Content-Type": "application/json"},
            param: params,
        });
        console.log("searchAsset 실행 완료");
   // }
};

// 로그아웃
const logout = async () => {
    try {
        const response = await axios.post('/login/logout.html', {}, {
            withCredentials: true
        });

        if (response.status === 200) {
            alert('로그아웃 되었습니다.');
            window.location.href = '/login/login.html';
        } else {
            alert('로그아웃 중 문제가 발생했습니다.');
            console.error('Logout failed with status:', response.status);
        }

    } catch (error) {
        // 네트워크 오류, 서버 오류 등 예외 처리
        console.error('로그아웃 중 오류 발생:', error);
        alert('로그아웃 중 서버 오류가 발생했습니다.');

        window.location.href = '/login/login.html';
    }
};

</script>


<style scoped>
</style>