diff --git a/backend/controllers/fileUploadController.js b/backend/controllers/fileUploadController.js index 03d1d7a..59fc43a 100644 --- a/backend/controllers/fileUploadController.js +++ b/backend/controllers/fileUploadController.js @@ -133,7 +133,7 @@ exports.uploadEventFile = (req, res) => { // 업로드된 파일 파싱 및 미리보기 데이터 제공 exports.parseEventFile = async (req, res) => { try { - const { filename, clubId } = req.body; + const { filename, clubId, sheetName } = req.body; if (!filename) { return res.status(400).json({ message: '파일명이 필요합니다.' }); @@ -196,7 +196,7 @@ exports.parseEventFile = async (req, res) => { // 파일 형식에 따른 처리 if (fileExt === '.xlsx' || fileExt === '.xls') { // Excel 파일 처리 - parsedData = await parseExcelFile(filePath, memberMap); + parsedData = await parseExcelFile(filePath, memberMap, sheetName); } else if (fileExt === '.csv') { // CSV 파일 처리 parsedData = await parseCsvFile(filePath, memberMap); @@ -227,9 +227,14 @@ exports.parseEventFile = async (req, res) => { }; // Excel 파일 파싱 함수 -async function parseExcelFile(filePath, memberMap) { +async function parseExcelFile(filePath, memberMap, requestedSheetName = null) { const workbook = xlsx.readFile(filePath, { type: 'binary', cellDates: true, codepage: 949 }); - const sheetName = workbook.SheetNames[0]; + + // 요청된 시트명이 있고 해당 시트가 존재하면 그것을 사용, 아니면 첫 번째 시트 사용 + const sheetName = (requestedSheetName && workbook.SheetNames.includes(requestedSheetName)) + ? requestedSheetName + : workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; const jsonData = xlsx.utils.sheet_to_json(worksheet, { header: 1, defval: '' }); diff --git a/backend/models/Member.js b/backend/models/Member.js index 1204e38..094da22 100644 --- a/backend/models/Member.js +++ b/backend/models/Member.js @@ -2,6 +2,7 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../config/database'); const User = require('./User'); const Club = require('./Club'); +const { encrypt, decrypt } = require('../utils/encryption'); const Member = sequelize.define('Member', { id: { @@ -53,12 +54,26 @@ const Member = sequelize.define('Member', { phone: { type: DataTypes.STRING, allowNull: true, - comment: '전화번호' + comment: '전화번호 (암호화됨)', + get() { + const rawValue = this.getDataValue('phone'); + return rawValue ? decrypt(rawValue) : null; + }, + set(value) { + this.setDataValue('phone', value ? encrypt(value) : null); + } }, email: { type: DataTypes.STRING, allowNull: true, - comment: '이메일' + comment: '이메일 (암호화됨)', + get() { + const rawValue = this.getDataValue('email'); + return rawValue ? decrypt(rawValue) : null; + }, + set(value) { + this.setDataValue('email', value ? encrypt(value) : null); + } }, joinDate: { type: DataTypes.DATE, diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js new file mode 100644 index 0000000..727cfaa --- /dev/null +++ b/backend/utils/encryption.js @@ -0,0 +1,61 @@ +const crypto = require('crypto'); +require('dotenv').config(); + +// 환경 변수에서 암호화 키 가져오기 +const RAW_ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '안전한_암호화_키_설정'; +// 키를 정확히 32바이트(256비트)로 변환 +let ENCRYPTION_KEY; +try { + // 동기적으로 키 생성 (초기화 시 한 번만 실행됨) + ENCRYPTION_KEY = crypto.createHash('sha256').update(RAW_ENCRYPTION_KEY).digest(); +} catch (error) { + console.error('암호화 키 생성 오류:', error); + throw new Error('암호화 키 생성 실패'); +} + +const IV_LENGTH = 16; // AES 블록 크기 + +/** + * 문자열을 암호화하는 함수 + * @param {string} text - 암호화할 텍스트 + * @returns {string} - 암호화된 텍스트 (base64 인코딩) + */ +function encrypt(text) { + if (!text) return null; + + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); + + let encrypted = cipher.update(text); + encrypted = Buffer.concat([encrypted, cipher.final()]); + + // IV와 암호화된 데이터를 함께 저장 (복호화 시 필요) + return iv.toString('hex') + ':' + encrypted.toString('hex'); +} + +/** + * 암호화된 문자열을 복호화하는 함수 + * @param {string} text - 복호화할 암호화된 텍스트 + * @returns {string} - 복호화된 원본 텍스트 + */ +function decrypt(text) { + if (!text) return null; + + const textParts = text.split(':'); + if (textParts.length !== 2) return null; + + const iv = Buffer.from(textParts[0], 'hex'); + const encryptedText = Buffer.from(textParts[1], 'hex'); + + const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); + + let decrypted = decipher.update(encryptedText); + decrypted = Buffer.concat([decrypted, decipher.final()]); + + return decrypted.toString(); +} + +module.exports = { + encrypt, + decrypt +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4ddb06a..2f6df79 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,7 @@ "vue-chartjs": "^5.3.2", "vue-router": "^4.5.0", "vuedraggable": "^4.1.0", + "xlsx": "^0.18.5", "yup": "^1.3.3" }, "devDependencies": { @@ -1828,6 +1829,15 @@ } } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1937,6 +1947,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chart.js": { "version": "4.4.9", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", @@ -1949,6 +1972,15 @@ "pnpm": ">=8" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1983,6 +2015,18 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2339,6 +2383,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -3201,6 +3254,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/strip-final-newline": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", @@ -3616,6 +3681,24 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -3637,6 +3720,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index da89795..17ee4cd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "vue-chartjs": "^5.3.2", "vue-router": "^4.5.0", "vuedraggable": "^4.1.0", + "xlsx": "^0.18.5", "yup": "^1.3.3" }, "devDependencies": { diff --git a/frontend/src/components/event/FileUploadDialog.vue b/frontend/src/components/event/FileUploadDialog.vue index cd351e4..ce6b294 100644 --- a/frontend/src/components/event/FileUploadDialog.vue +++ b/frontend/src/components/event/FileUploadDialog.vue @@ -28,6 +28,7 @@
- + + +
+ + + +