초기 PM 소스 전체 업로드

This commit is contained in:
koj729
2026-06-12 17:14:03 +09:00
commit 4e33c9a02a
1769 changed files with 377797 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
# 1. 공통 애플리케이션 설정
NODE_ENV=development
SERVICE_NAME=PM_ver4_LOCAL
DEPLOYMENT_TYPE=ONPREMISE
COOKIE_SECRET=local_pm_development_secret_key_9988
PROJECT_VERSION=1.0.0
# 2. 로컬 서버 실행 포트 및 호스트
LOCAL_IP=localhost
LOCAL_PORT=6565
# 3. PostgreSQL DB 설정 (ONPREMISE 기준 - Docker 연동)
ONPREMISE_POSTGRES_HOST=localhost
ONPREMISE_POSTGRES_PORT=5432
ONPREMISE_POSTGRES_DATABASE=pm_db
ONPREMISE_POSTGRES_USER=postgres
ONPREMISE_POSTGRES_PASSWORD=your_password
# 4. Redis 설정 (Docker 연동)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# 5. MinIO 로컬 S3 스토리지 설정 (Docker 연동)
MINIO_ENDPOINT=http://localhost:9000
MINIO_ACCESSKEYID=minio_access_key
MINIO_SECRETACCESSKEY=minio_secret_key
# 6. OAuth (Sentinel SSO) 서버 임시 설정 (로컬 우회 사용 예정)
SENTINEL_BASE=http://localhost:6565
CLIENT_ID=PM_LOCAL
# 7. Gemini API (필요시 기입)
GEMINI_API_KEY=AQ.Ab8RN6KjoJggmdVeZ6ag87t2fuRjCKMr5xAQh8yBo3aYNZ82aQ
+1
View File
@@ -0,0 +1 @@
node_modules
Binary file not shown.
@@ -0,0 +1 @@
_업데이트 제외 - libs, logs, node_module, programs, .env
@@ -0,0 +1 @@
_업데이트 시 주의사항 - 제외 폴더 내 변경된 내용 있는 경우 해당 파일만 교체
+198
View File
@@ -0,0 +1,198 @@
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const dotenv = require('dotenv');
dotenv.config();
const bodyParser = require('body-parser');
const dbConnect = require('./db/index');
const cors = require('cors');
const passport = require('passport');
// const FileStore = require('session-file-store')(session);
const helmet = require('helmet');
const mainRouter = require('./routes/mainRouter');
const archiveRouter = require('./routes/archiveRouter');
const authRouter = require('./routes/authRouter.js');
const passportConfig = require('./passport/index.js');
const commonRouter = require('./routes/commonRouter.js');
const officialDocRouter = require('./routes/officialDocRouter.js');
const overviewRouter = require('./routes/overviewRouter.js');
const bullBoardRouter = require('./routes/bullBoardRouter.js');
const gsimRouter = require('./routes/gsimRouter.js');
//test
const oauthRouter = require('./oauth/oauthRouter.js');
const { isLoggedIn, deserializeUser } = require('./oauth/oauthController');
const logger = require('./logger');
const app = express();
const env = process.env.NODE_ENV;
dbConnect(); // DB 연결
passportConfig(); // passport 설정
// app.use(
// helmet({
// contentSecurityPolicy: false, // CSP는 비활성화
// crossOriginEmbedderPolicy: false, // 외부 CDN 자원 쓰면 충돌 날 수 있음
// })
// );
const ALLOWED_PARENTS = [
"http://bcmf.hanmaceng.co.kr",
"https://bcmf.hanmaceng.co.kr",
"http://*.hanmaceng.co.kr",
"https://*.hanmaceng.co.kr",
];
app.use(helmet({
frameguard: false, // X-Frame-Options 제거
contentSecurityPolicy: false, // CSP는 직접 세팅
crossOriginEmbedderPolicy: false,
}));
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
`frame-ancestors 'self' ${ALLOWED_PARENTS.join(' ')}`
);
next();
});
//페이로드 크기 설정 => upload check할때 json 용량때문에 사용
app.use(bodyParser.json({limit : '50mb'}));
app.use(bodyParser.urlencoded({limit : '50mb', extended:true}));
app.use(cors());
app.use(morgan('dev'));
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
app.use('/libs', express.static(path.join(__dirname, 'libs')));
app.use('/', express.static(path.join(__dirname, 'views')));
app.use(express.static(__dirname + '/node_modules/socket.io/client-dist'));
app.use('/:projectId',express.static(__dirname + '/node_modules/socket.io/client-dist'));
// app.use('/PM_ver4', express.static(env != 'production'?'D:\\PM_ver4':'D:\\PM_ver4')); // D드라이브 폴더 사용
// 동적 API 및 라우트 캐싱 방지 설정 (304 Cache로 인한 이전 에러 응답 물림 및 리다이렉트 캐싱 방지)
app.use((req, res, next) => {
if (req.method === 'GET' && !req.path.includes('.') && !req.path.startsWith('/node_modules') && !req.path.startsWith('/libs')) {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
next();
});
app.use(express.json());
app.use(express.urlencoded({extended:false}));
app.use(cookieParser(process.env.COOKIE_SECRET));
// const fileStoreOptions = {
// path:`C:\\develop\\session`,
// ttl:1000*60*60*24,
// logFn: function(){},
// retries:5
// }
// app.use(session({
// store: new FileStore(fileStoreOptions), // FileStore를 사용하도록 설정
// secret: 'testkey', // 세션 ID를 서명하는 데 사용되는 비밀 키 (필수)
// resave: false, // 세션 데이터가 변경되지 않아도 세션을 다시 저장할지 여부
// saveUninitialized: false, // 초기화되지 않은 세션을 저장소에 저장할지 여부
// cookie: {
// maxAge: 1000 * 60 * 60 * 24 // 24시간 후 쿠키 만료
// }
// }));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
// maxAge: (24 * 60 * 60 * 1000) + (9 * 3600 * 1000)
}
}));
//passport 초기화
app.use(passport.initialize()); // passport 설정을 심음
app.use(passport.session()); // req.session에 passport 정보를 저장
app.post('/log-client-error', express.json(), (req, res) => {
const { message, source, lineno, colno, stack } = req.body;
console.error('🚨 [CLIENT ERROR]:', message);
console.error(` at ${source}:${lineno}:${colno}`);
if (stack) console.error(stack);
res.sendStatus(200);
});
// 라우터
app.use(`/oauth`, oauthRouter);
app.use('/login', (req, res) => {
res.redirect('/user/login');
});
app.use('/user/login', (req, res, next) =>{res.sendFile(path.join(process.cwd()+`/views/login/login.html`))});
// app.use('/popup' ,(req,res,next)=>{res.sendFile(path.join(process.cwd()+`/views/main/popup.html`))})
// 글로벌 사용자 세션 역직렬화 (로그인 상태 복원)
app.use(deserializeUser);
// 공공 라우트 및 인증 API
app.use('/auth', authRouter);
// 어드민 화면 서빙 및 권한 통제
const isAdminLocal = (req, res, next) => {
const userGroup = req.user?.group;
if (req.user && (userGroup === 'USER_GROUP_super' || userGroup === 'dev' || userGroup === 'super')) {
return next();
}
return res.status(403).send("어드민(super) 권한이 필요합니다.");
};
app.get('/admin', isLoggedIn, isAdminLocal, (req, res) => {
res.sendFile(path.join(process.cwd(), 'views/admin/dashboard.html'));
});
// 로그인 보호 장벽 적용 (이하 라우트는 로그인 세션 필수)
app.use(isLoggedIn);
app.use('/', mainRouter);
app.use('/:projectId/archive', archiveRouter);
app.use('/:projectId/overview', overviewRouter);
app.use('/:projectId/officialDoc', officialDocRouter);
app.use('/common', commonRouter);
app.use('/gsim', gsimRouter);
// 어드민 전용 REST API
app.use('/api/admin', require('./routes/admin/adminRouter'));
// BullMQ 모니터링 대시보드
app.use('/admin/queues', bullBoardRouter);
// 404응답 미들웨어
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없어요`);
error.status = 404;
logger.error(error.message);
next(error);
});
// 에러 처리 미들웨어
app.use((err, req, res, next) => {
console.error(err.stack);
logger.error(err.message);
res.status(err.status || 500).send(`status code: ${err.status || 500} 에러가 났어요`)
});
// 자동 보관 및 자동 삭제 스케줄러 가동
const scheduler = require('./libs/scheduler');
scheduler.start();
module.exports = app;
+14
View File
@@ -0,0 +1,14 @@
const { S3Client } = require('@aws-sdk/client-s3');
require('dotenv').config();
// CloudFlare R2
const cloudClient = new S3Client({
region: 'auto',
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESSKEYID,
secretAccessKey: process.env.R2_SECRETACCESSKEY,
},
});
module.exports = cloudClient;
+29
View File
@@ -0,0 +1,29 @@
const { S3Client } = require('@aws-sdk/client-s3');
require('dotenv').config();
// minIO
const onPremiseClient = new S3Client({
region: 'auto',
endpoint: process.env.MINIO_ENDPOINT,
forcePathStyle: true, // 중요! MinIO는 path-style 요청을 기본으로 사용
credentials: {
accessKeyId: process.env.MINIO_ACCESSKEYID,
secretAccessKey: process.env.MINIO_SECRETACCESSKEY,
},
});
// 로컬 MinIO는 대문자 버킷명을 허용하지 않으므로, 모든 S3 요청의 Bucket 파라미터를 소문자로 변환하는 미들웨어 주입
onPremiseClient.middlewareStack.add(
(next, context) => async (args) => {
if (args.input && args.input.Bucket) {
args.input.Bucket = args.input.Bucket.toLowerCase().replaceAll('_', '-');
}
return next(args);
},
{
step: 'initialize',
name: 'lowercaseBucketName'
}
);
module.exports = onPremiseClient;
+18
View File
@@ -0,0 +1,18 @@
const Redis = require('ioredis');
require('dotenv').config();
const redisConnection = new Redis({
host: process.env.REDIS_HOST,
port: +process.env.REDIS_PORT,
maxRetriesPerRequest: null,
password: process.env.REDIS_PASSWORD
// tls:{
// }
});
// await redisConnection.del('bull:convert-pdf:id');
redisConnection.on('connect', () => console.log('✔️ Redis connected'));
redisConnection.on('error', (err) => console.error('❌ Redis error', err));
module.exports = { redisConnection };
+904
View File
@@ -0,0 +1,904 @@
const pool = require("../../db/pool.js");
const crypto = require("crypto");
const env = process.env.NODE_ENV;
const tbProject = env === 'production' ? 'tb_project' : '_test_tb_project';
const tbData = env === 'production' ? 'tb_data' : '_test_tb_data';
const tbLog = 'tb_log';
const tbPermission = env === 'production' ? 'tb_permission' : '_test_tb_permission';
// 감사 로그(Audit Log) 삽입 헬퍼 함수 (메인 트랜잭션에 영향을 주지 않기 위해 pool.query 사용)
async function insertAuditLog(projectId, activity, userId, userIp, detailsArray) {
try {
let targetProjectId = projectId;
if (targetProjectId === 'SYSTEM' || !targetProjectId) {
targetProjectId = null;
} else {
// 실제로 존재하는 프로젝트인지 더블 체크 (FK 에러 방지)
const checkRes = await pool.query(`SELECT 1 FROM ver4.${tbProject} WHERE project_id = $1`, [targetProjectId]);
if (checkRes.rows.length === 0) {
targetProjectId = null;
}
}
const query = `
INSERT INTO ver4.${tbLog} (project_id, activity, user_id, user_ip, log_date, path_arr)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, $5);
`;
await pool.query(query, [
targetProjectId,
activity,
userId || 'unknown_admin',
userIp || '0.0.0.0',
detailsArray || []
]);
} catch (logErr) {
console.error("🚨 [insertAuditLog] Audit log insert failed:", logErr);
}
}
// 1. 프로젝트 관리 (Projects)
exports.getProjects = async (req, res) => {
const client = await pool.connect();
try {
const query = `
SELECT
p.*,
cd.code_nm as category_nm,
COALESCE(d.total_size, 0)::BIGINT as used_bytes,
COALESCE(d.file_count, 0)::INTEGER as file_count
FROM ver4.${tbProject} p
LEFT JOIN ver4.code_detail cd ON cd.main_code = 'PROJECT_CATEGORY' AND p.category = cd.sub_code
LEFT JOIN (
SELECT project_id, SUM(COALESCE(data_size, 0)) as total_size, COUNT(*) as file_count
FROM ver4.${tbData}
WHERE is_folder = false AND (is_removed = false OR is_removed IS NULL)
GROUP BY project_id
) d ON p.project_id = d.project_id
ORDER BY p.project_id;
`;
const result = await client.query(query);
res.status(200).json(result.rows);
} catch (err) {
console.error("getProjects Error:", err);
res.status(500).json({ error: "프로젝트 목록 조회 실패" });
} finally {
client.release();
}
};
exports.createProject = async (req, res) => {
const { project_id, project_nm, short_nm, category, limit_storage, is_active } = req.body;
if (!project_id || !project_nm) {
return res.status(400).json({ error: "프로젝트 ID와 명칭은 필수입니다." });
}
const client = await pool.connect();
try {
// 중복 체크
const dupRes = await client.query(`SELECT 1 FROM ver4.${tbProject} WHERE project_id = $1`, [project_id]);
if (dupRes.rows.length > 0) {
return res.status(400).json({ error: "이미 존재하는 프로젝트 ID입니다." });
}
// storage_byte 계산 (GB -> Bytes)
const storage_byte = limit_storage ? parseInt(limit_storage) * 1024 * 1024 * 1024 : 0;
const query = `
INSERT INTO ver4.${tbProject} (project_id, project_nm, short_nm, category, storage_byte, is_active, user_id, create_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP)
RETURNING *;
`;
const result = await client.query(query, [
project_id,
project_nm,
short_nm || null,
category || null,
storage_byte,
is_active ?? true,
req.user?.user_id || 'admin'
]);
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(project_id, 'createProject', req.user?.user_id, userIp, [
`Project Name: ${project_nm}`,
`Category: ${category}`,
`Storage limit: ${limit_storage} GB`
]);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error("createProject Error:", err);
res.status(500).json({ error: "프로젝트 생성 실패" });
} finally {
client.release();
}
};
exports.updateProject = async (req, res) => {
const { id } = req.params;
const { project_nm, short_nm, category, limit_storage, is_active } = req.body;
const client = await pool.connect();
try {
const storage_byte = limit_storage ? parseInt(limit_storage) * 1024 * 1024 * 1024 : 0;
const query = `
UPDATE ver4.${tbProject}
SET project_nm = $1, short_nm = $2, category = $3, storage_byte = $4, is_active = $5
WHERE project_id = $6
RETURNING *;
`;
const result = await client.query(query, [project_nm, short_nm || null, category || null, storage_byte, is_active, id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(id, 'updateProject', req.user?.user_id, userIp, [
`Project Name: ${project_nm}`,
`Category: ${category}`,
`Storage limit: ${limit_storage} GB`,
`Active status: ${is_active}`
]);
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("updateProject Error:", err);
res.status(500).json({ error: "프로젝트 수정 실패" });
} finally {
client.release();
}
};
exports.deleteProject = async (req, res) => {
const { id } = req.params;
const client = await pool.connect();
try {
// [3대 삭제 제한 1] 프로젝트 사용 이력 체크
// tb_data 사용 이력 검사
const dataCountRes = await client.query(`SELECT COUNT(*) FROM ver4.${tbData} WHERE project_id = $1`, [id]);
if (parseInt(dataCountRes.rows[0].count) > 0) {
return res.status(400).json({ error: `해당 현장에 업로드된 파일 데이터(${dataCountRes.rows[0].count}건)가 존재하여 프로젝트를 삭제할 수 없습니다.` });
}
// tb_official_doc_file 사용 이력 검사
const docCountRes = await client.query("SELECT COUNT(*) FROM ver4.tb_official_doc_file WHERE project_id = $1", [id]);
if (parseInt(docCountRes.rows[0].count) > 0) {
return res.status(400).json({ error: `해당 현장에 연결된 공문서 파일(${docCountRes.rows[0].count}건)이 존재하여 프로젝트를 삭제할 수 없습니다.` });
}
// tb_banner_notice 사용 이력 검사
const noticeCountRes = await client.query("SELECT COUNT(*) FROM ver4.tb_banner_notice WHERE project_id = $1", [id]);
if (parseInt(noticeCountRes.rows[0].count) > 0) {
return res.status(400).json({ error: `해당 현장 전용 배너 공지 이력(${noticeCountRes.rows[0].count}건)이 존재하여 프로젝트를 삭제할 수 없습니다.` });
}
// 통과 시 삭제 수행 (tb_permission 등 Cascade 관계는 수동으로 정리 가능하나, 안전을 위해 권한도 함께 정리함)
await client.query(`DELETE FROM ver4.${tbPermission} WHERE project_id = $1`, [id]);
const result = await client.query(`DELETE FROM ver4.${tbProject} WHERE project_id = $1 RETURNING *`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(id, 'deleteProject', req.user?.user_id, userIp, [
`Deleted project name: ${result.rows[0].project_nm}`
]);
res.status(200).json({ message: "프로젝트가 정상적으로 삭제되었습니다." });
} catch (err) {
console.error("deleteProject Error:", err);
res.status(500).json({ error: "프로젝트 삭제 실패" });
} finally {
client.release();
}
};
// 2. 프로젝트 권한 배정 (Permissions)
exports.getProjectPermissions = async (req, res) => {
const { projectId } = req.params;
const client = await pool.connect();
try {
const query = `
SELECT pm.project_id, pm.user_id, pm.lev, u.user_nm, u.company, u.dept, u.position
FROM ver4.${tbPermission} pm
JOIN ver4.tb_user u ON pm.user_id = u.user_id
WHERE pm.project_id = $1
ORDER BY u.user_nm ASC;
`;
const result = await client.query(query, [projectId]);
res.status(200).json(result.rows);
} catch (err) {
console.error("getProjectPermissions Error:", err);
res.status(500).json({ error: "권한 목록 조회 실패" });
} finally {
client.release();
}
};
exports.assignPermissions = async (req, res) => {
const { project_id, users } = req.body; // users: [{ user_id, lev }]
if (!project_id || !users || !Array.isArray(users) || users.length === 0) {
return res.status(400).json({ error: "잘못된 요청 파라미터입니다." });
}
const client = await pool.connect();
try {
await client.query("BEGIN");
for (const user of users) {
await client.query(`
INSERT INTO ver4.${tbPermission} (project_id, user_id, lev)
VALUES ($1, $2, $3)
ON CONFLICT (project_id, user_id)
DO UPDATE SET lev = EXCLUDED.lev;
`, [project_id, user.user_id, user.lev]);
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(project_id, 'assignPermission', req.user?.user_id, userIp, [
`Assigned user_id: ${user.user_id}`,
`Level assigned: ${user.lev}`
]);
}
await client.query("COMMIT");
res.status(200).json({ message: "사용자가 성공적으로 현장에 배정되었습니다." });
} catch (err) {
await client.query("ROLLBACK");
console.error("assignPermissions Error:", err);
res.status(500).json({ error: "사용자 권한 배정 실패" });
} finally {
client.release();
}
};
exports.updatePermission = async (req, res) => {
const { project_id, user_id, lev } = req.body;
if (!project_id || !user_id || lev === undefined) {
return res.status(400).json({ error: "필수 파라미터가 누락되었습니다." });
}
const client = await pool.connect();
try {
const query = `
UPDATE ver4.${tbPermission}
SET lev = $1
WHERE project_id = $2 AND user_id = $3
RETURNING *;
`;
const result = await client.query(query, [lev, project_id, user_id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(project_id, 'updatePermission', req.user?.user_id, userIp, [
`Updated user_id: ${user_id}`,
`New level: ${lev}`
]);
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("updatePermission Error:", err);
res.status(500).json({ error: "권한 등급 수정 실패" });
} finally {
client.release();
}
};
exports.removePermission = async (req, res) => {
const { project_id, user_id } = req.body;
if (!project_id || !user_id) {
return res.status(400).json({ error: "필수 파라미터가 누락되었습니다." });
}
const client = await pool.connect();
try {
const query = `
DELETE FROM ver4.${tbPermission}
WHERE project_id = $1 AND user_id = $2
RETURNING *;
`;
const result = await client.query(query, [project_id, user_id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(project_id, 'removePermission', req.user?.user_id, userIp, [
`Removed user_id: ${user_id}`
]);
res.status(200).json({ message: "사용자 배정이 제외되었습니다." });
} catch (err) {
console.error("removePermission Error:", err);
res.status(500).json({ error: "사용자 배정 제외 실패" });
} finally {
client.release();
}
};
// 3. 실시간 배너 공지 (Banners)
exports.getBanners = async (req, res) => {
const { status, fromDate, toDate } = req.query;
const client = await pool.connect();
try {
let query = `
SELECT b.*, p.project_nm, cd.code_nm as status_nm
FROM ver4.tb_banner_notice b
LEFT JOIN ver4.${tbProject} p ON b.project_id = p.project_id
LEFT JOIN ver4.code_detail cd ON b.status_code = cd.base_code
WHERE 1=1
`;
const params = [];
let paramIndex = 1;
if (status && status !== 'all') {
query += ` AND b.status_code = $${paramIndex++}`;
params.push(status);
}
if (fromDate) {
query += ` AND b.reg_date >= $${paramIndex++}`;
params.push(fromDate);
}
if (toDate) {
query += ` AND b.reg_date <= $${paramIndex++}`;
params.push(toDate);
}
query += ` ORDER BY b.banner_id DESC;`;
const result = await client.query(query, params);
res.status(200).json(result.rows);
} catch (err) {
console.error("getBanners Error:", err);
res.status(500).json({ error: "배너 공지 목록 조회 실패" });
} finally {
client.release();
}
};
exports.createBanner = async (req, res) => {
const { project_id, start_date, end_date, notice_text } = req.body;
if (!start_date || !end_date || !notice_text) {
return res.status(400).json({ error: "시작일, 종료일, 공지 자막은 필수입니다." });
}
const client = await pool.connect();
try {
// 송출 상태 계산 (오늘 기준)
const today = new Date().toISOString().split('T')[0];
let status_code = 'NOTICE_STATUS_scheduled';
if (today >= start_date && today <= end_date) {
status_code = 'NOTICE_STATUS_active';
} else if (today > end_date) {
status_code = 'NOTICE_STATUS_expired';
}
const query = `
INSERT INTO ver4.tb_banner_notice (project_id, start_date, end_date, notice_text, status_code, reg_date)
VALUES ($1, $2, $3, $4, $5, CURRENT_DATE)
RETURNING *;
`;
const result = await client.query(query, [
project_id === 'all' || !project_id ? null : project_id,
start_date,
end_date,
notice_text,
status_code
]);
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(project_id === 'all' || !project_id ? 'SYSTEM' : project_id, 'createBanner', req.user?.user_id, userIp, [
`Banner text: ${notice_text}`,
`Period: ${start_date} ~ ${end_date}`
]);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error("createBanner Error:", err);
res.status(500).json({ error: "배너 공지 생성 실패" });
} finally {
client.release();
}
};
exports.stopBanner = async (req, res) => {
const { id } = req.params;
const client = await pool.connect();
try {
const query = `
UPDATE ver4.tb_banner_notice
SET status_code = 'NOTICE_STATUS_expired'
WHERE banner_id = $1
RETURNING *;
`;
const result = await client.query(query, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(result.rows[0].project_id || 'SYSTEM', 'stopBanner', req.user?.user_id, userIp, [
`Stopped banner_id: ${id}`,
`Banner text: ${result.rows[0].notice_text}`
]);
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("stopBanner Error:", err);
res.status(500).json({ error: "배너 송출 중지 실패" });
} finally {
client.release();
}
};
// 4. 사용자 관리 (Users)
exports.getUsers = async (req, res) => {
const client = await pool.connect();
try {
const query = `
SELECT u.user_id, u.user_nm, u.company, u.dept, u.position, u.group, u.is_resigned, u.create_date, cd.code_nm as group_nm
FROM ver4.tb_user u
LEFT JOIN ver4.code_detail cd ON u.group = cd.base_code
ORDER BY u.user_id;
`;
const result = await client.query(query);
res.status(200).json(result.rows);
} catch (err) {
console.error("getUsers Error:", err);
res.status(500).json({ error: "사용자 목록 조회 실패" });
} finally {
client.release();
}
};
exports.getUserPermissions = async (req, res) => {
const { id } = req.params;
const client = await pool.connect();
try {
const query = `
SELECT pm.project_id, p.project_nm, pm.lev
FROM ver4.${tbPermission} pm
JOIN ver4.${tbProject} p ON pm.project_id = p.project_id
WHERE pm.user_id = $1
ORDER BY p.project_nm ASC;
`;
const result = await client.query(query, [id]);
res.status(200).json(result.rows);
} catch (err) {
console.error("getUserPermissions Error:", err);
res.status(500).json({ error: "사용자 참여 현장 조회 실패" });
} finally {
client.release();
}
};
exports.createUser = async (req, res) => {
const { user_id, user_nm, user_pw, company, dept, position, group, is_resigned } = req.body;
if (!user_id || !user_nm || !user_pw) {
return res.status(400).json({ error: "사용자 ID, 이름, 비밀번호는 필수입니다." });
}
const client = await pool.connect();
try {
// 중복 체크
const dupRes = await client.query("SELECT 1 FROM ver4.tb_user WHERE user_id = $1", [user_id]);
if (dupRes.rows.length > 0) {
return res.status(400).json({ error: "이미 존재하는 사용자 ID입니다." });
}
// 비밀번호 해싱 (SHA256 - GSIM 연동 상의 DB 제약 조건을 채우기 위함)
const passwordHash = crypto.createHash('sha256').update(user_pw).digest('hex');
const query = `
INSERT INTO ver4.tb_user (user_id, user_nm, user_pw, company, dept, position, "group", is_resigned, create_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP)
RETURNING *;
`;
const result = await client.query(query, [
user_id,
user_nm,
passwordHash,
company || null,
dept || null,
position || null,
group || null,
is_resigned ?? false
]);
const user = result.rows[0];
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'createUser', req.user?.user_id, userIp, [
`Created user_id: ${user_id}`,
`User name: ${user_nm}`,
`Group: ${group}`
]);
user.user_pw = undefined; // 비밀번호 제외
res.status(201).json(user);
} catch (err) {
console.error("createUser Error:", err);
res.status(500).json({ error: "사용자 생성 실패" });
} finally {
client.release();
}
};
exports.updateUser = async (req, res) => {
const { id } = req.params;
const { user_nm, company, dept, position, group, is_resigned } = req.body;
const client = await pool.connect();
try {
const query = `
UPDATE ver4.tb_user
SET user_nm = $1, company = $2, dept = $3, position = $4, "group" = $5, is_resigned = $6
WHERE user_id = $7
RETURNING *;
`;
const result = await client.query(query, [
user_nm,
company || null,
dept || null,
position || null,
group || null,
is_resigned,
id
]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const user = result.rows[0];
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'updateUser', req.user?.user_id, userIp, [
`Updated user_id: ${id}`,
`User name: ${user_nm}`,
`Group: ${group}`,
`Is resigned: ${is_resigned}`
]);
user.user_pw = undefined;
res.status(200).json(user);
} catch (err) {
console.error("updateUser Error:", err);
res.status(500).json({ error: "사용자 정보 수정 실패" });
} finally {
client.release();
}
};
exports.deleteUser = async (req, res) => {
const { id } = req.params;
const client = await pool.connect();
try {
// [3대 삭제 제한 2] 사용자 삭제 시 권한 테이블 배정 정보 체크
const permCountRes = await client.query(`SELECT COUNT(*) FROM ver4.${tbPermission} WHERE user_id = $1`, [id]);
if (parseInt(permCountRes.rows[0].count) > 0) {
return res.status(400).json({ error: `해당 사용자가 참여 중인 현장 권한(${permCountRes.rows[0].count}건)이 존재하여 계정을 삭제할 수 없습니다. 배정 해제 후 삭제 가능합니다.` });
}
const result = await client.query("DELETE FROM ver4.tb_user WHERE user_id = $1 RETURNING *", [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'deleteUser', req.user?.user_id, userIp, [
`Deleted user_id: ${id}`
]);
res.status(200).json({ message: "사용자 계정이 성공적으로 삭제되었습니다." });
} catch (err) {
console.error("deleteUser Error:", err);
res.status(500).json({ error: "사용자 삭제 실패" });
} finally {
client.release();
}
};
// 5. 감사 로그 조회 (Audit Logs)
exports.getAuditLogs = async (req, res) => {
const { user_id, activity } = req.query;
const client = await pool.connect();
try {
let query = `
SELECT log_id, log_date as clean_date, project_id, user_id, user_ip, activity as clean_path, path_arr as criteria_info
FROM ver4.${tbLog}
WHERE 1=1
`;
const params = [];
let paramIndex = 1;
if (user_id) {
query += ` AND user_id ILIKE $${paramIndex++}`;
params.push(`%${user_id}%`);
}
if (activity && activity !== 'all') {
query += ` AND activity = $${paramIndex++}`;
params.push(activity);
}
query += ` ORDER BY log_id DESC LIMIT 100;`;
const result = await client.query(query, params);
res.status(200).json(result.rows);
} catch (err) {
console.error("getAuditLogs Error:", err);
res.status(500).json({ error: "감사 로그 조회 실패" });
} finally {
client.release();
}
};
// 6. 보관 정책 설정 (Policies)
exports.getSystemPolicy = async (req, res) => {
const client = await pool.connect();
try {
const result = await client.query("SELECT * FROM ver4.tb_system_policy WHERE policy_key = 'GLOBAL_DELETE_POLICY'");
if (result.rows.length === 0) {
// 기본 레코드 없을 시 생성해서 전송
const insertRes = await client.query(`
INSERT INTO ver4.tb_system_policy (policy_key, limit_file_count, limit_days, is_active)
VALUES ('GLOBAL_DELETE_POLICY', 100, 30, FALSE)
RETURNING *;
`);
return res.status(200).json(insertRes.rows[0]);
}
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("getSystemPolicy Error:", err);
res.status(500).json({ error: "보관 정책 조회 실패" });
} finally {
client.release();
}
};
exports.updateSystemPolicy = async (req, res) => {
const { limit_file_count, limit_days, is_active } = req.body;
if (limit_file_count === undefined || limit_days === undefined || is_active === undefined) {
return res.status(400).json({ error: "필수 정책 입력값이 누락되었습니다." });
}
const client = await pool.connect();
try {
const query = `
INSERT INTO ver4.tb_system_policy (policy_key, limit_file_count, limit_days, is_active, upd_date)
VALUES ('GLOBAL_DELETE_POLICY', $1, $2, $3, CURRENT_TIMESTAMP)
ON CONFLICT (policy_key)
DO UPDATE SET limit_file_count = EXCLUDED.limit_file_count,
limit_days = EXCLUDED.limit_days,
is_active = EXCLUDED.is_active,
upd_date = CURRENT_TIMESTAMP
RETURNING *;
`;
const result = await client.query(query, [limit_file_count, limit_days, is_active]);
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'updateSystemPolicy', req.user?.user_id, userIp, [
`Limit file count: ${limit_file_count}`,
`Limit days: ${limit_days}`,
`Is active: ${is_active}`
]);
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("updateSystemPolicy Error:", err);
res.status(500).json({ error: "보관 정책 수정 실패" });
} finally {
client.release();
}
};
exports.getAutoCleanLogs = async (req, res) => {
const client = await pool.connect();
try {
const query = `
SELECT log_id, clean_date, project_id, clean_path, criteria_info, result_status
FROM ver4.tb_auto_clean_log
ORDER BY log_id DESC LIMIT 100;
`;
const result = await client.query(query);
res.status(200).json(result.rows);
} catch (err) {
console.error("getAutoCleanLogs Error:", err);
res.status(500).json({ error: "배치 이력 조회 실패" });
} finally {
client.release();
}
};
// 7. 공통 코드 관리 (Common Codes)
exports.getCodeMasters = async (req, res) => {
const client = await pool.connect();
try {
const query = "SELECT * FROM ver4.code_master ORDER BY main_code;";
const result = await client.query(query);
res.status(200).json(result.rows);
} catch (err) {
console.error("getCodeMasters Error:", err);
res.status(500).json({ error: "대분류 마스터 코드 조회 실패" });
} finally {
client.release();
}
};
exports.createCodeMaster = async (req, res) => {
const { main_code, main_code_nm, use_yn, rmk } = req.body;
if (!main_code || !main_code_nm) {
return res.status(400).json({ error: "대분류 코드와 명칭은 필수입니다." });
}
const client = await pool.connect();
try {
const dupRes = await client.query("SELECT 1 FROM ver4.code_master WHERE main_code = $1", [main_code]);
if (dupRes.rows.length > 0) {
return res.status(400).json({ error: "이미 존재하는 대분류 코드입니다." });
}
const query = `
INSERT INTO ver4.code_master (main_code, main_code_nm, use_yn, rmk)
VALUES ($1, $2, $3, $4)
RETURNING *;
`;
const result = await client.query(query, [main_code, main_code_nm, use_yn ?? 'Y', rmk || null]);
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'createCodeMaster', req.user?.user_id, userIp, [
`Master code: ${main_code}`,
`Master code name: ${main_code_nm}`
]);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error("createCodeMaster Error:", err);
res.status(500).json({ error: "대분류 등록 실패" });
} finally {
client.release();
}
};
exports.updateCodeMaster = async (req, res) => {
const { code } = req.params;
const { main_code_nm, use_yn, rmk } = req.body;
const client = await pool.connect();
try {
const query = `
UPDATE ver4.code_master
SET main_code_nm = $1, use_yn = $2, rmk = $3
WHERE main_code = $4
RETURNING *;
`;
const result = await client.query(query, [main_code_nm, use_yn, rmk || null, code]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'updateCodeMaster', req.user?.user_id, userIp, [
`Updated master code: ${code}`,
`New code name: ${main_code_nm}`,
`Use YN: ${use_yn}`
]);
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("updateCodeMaster Error:", err);
res.status(500).json({ error: "대분류 수정 실패" });
} finally {
client.release();
}
};
exports.deleteCodeMaster = async (req, res) => {
const { code } = req.params;
const client = await pool.connect();
try {
// [3대 삭제 제한 3] 대분류 삭제 시 하위 세부 코드 존재 체크
const detailCountRes = await client.query("SELECT COUNT(*) FROM ver4.code_detail WHERE main_code = $1", [code]);
if (parseInt(detailCountRes.rows[0].count) > 0) {
return res.status(400).json({ error: `해당 대분류에 기속된 세부 소분류 코드(${detailCountRes.rows[0].count}건)가 존재하여 대분류를 삭제할 수 없습니다.` });
}
const result = await client.query("DELETE FROM ver4.code_master WHERE main_code = $1 RETURNING *", [code]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'deleteCodeMaster', req.user?.user_id, userIp, [
`Deleted master code: ${code}`
]);
res.status(200).json({ message: "대분류 코드가 삭제되었습니다." });
} catch (err) {
console.error("deleteCodeMaster Error:", err);
res.status(500).json({ error: "대분류 삭제 실패" });
} finally {
client.release();
}
};
exports.getCodeDetails = async (req, res) => {
const { mainCode } = req.params;
const client = await pool.connect();
try {
const query = "SELECT * FROM ver4.code_detail WHERE main_code = $1 ORDER BY sort_ord, sub_code;";
const result = await client.query(query, [mainCode]);
res.status(200).json(result.rows);
} catch (err) {
console.error("getCodeDetails Error:", err);
res.status(500).json({ error: "소분류 세부 코드 조회 실패" });
} finally {
client.release();
}
};
exports.createCodeDetail = async (req, res) => {
const { main_code, sub_code, code_nm, sort_ord, use_yn, rmk } = req.body;
if (!main_code || !sub_code || !code_nm) {
return res.status(400).json({ error: "대분류, 소분류 코드 및 코드 명칭은 필수입니다." });
}
const client = await pool.connect();
try {
const dupRes = await client.query("SELECT 1 FROM ver4.code_detail WHERE main_code = $1 AND sub_code = $2", [main_code, sub_code]);
if (dupRes.rows.length > 0) {
return res.status(400).json({ error: "해당 대분류 내에 이미 존재하는 소분류 코드입니다." });
}
// 조합 코드 (base_code) 자동 조합: main_code || '_' || sub_code
const base_code = `${main_code}_${sub_code}`;
const query = `
INSERT INTO ver4.code_detail (main_code, sub_code, base_code, code_nm, sort_ord, use_yn, rmk)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *;
`;
const result = await client.query(query, [main_code, sub_code, base_code, code_nm, sort_ord ?? 1, use_yn ?? 'Y', rmk || null]);
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'createCodeDetail', req.user?.user_id, userIp, [
`Master code: ${main_code}`,
`Sub code: ${sub_code}`,
`Code name: ${code_nm}`
]);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error("createCodeDetail Error:", err);
res.status(500).json({ error: "소분류 등록 실패" });
} finally {
client.release();
}
};
exports.updateCodeDetail = async (req, res) => {
const { mainCode, subCode } = req.params;
const { code_nm, sort_ord, use_yn, rmk } = req.body;
const client = await pool.connect();
try {
const query = `
UPDATE ver4.code_detail
SET code_nm = $1, sort_ord = $2, use_yn = $3, rmk = $4
WHERE main_code = $5 AND sub_code = $6
RETURNING *;
`;
const result = await client.query(query, [code_nm, sort_ord, use_yn, rmk || null, mainCode, subCode]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'updateCodeDetail', req.user?.user_id, userIp, [
`Master code: ${mainCode}`,
`Sub code: ${subCode}`,
`New code name: ${code_nm}`
]);
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("updateCodeDetail Error:", err);
res.status(500).json({ error: "소분류 수정 실패" });
} finally {
client.release();
}
};
// 7-4. 소분류 코드 삭제
exports.deleteCodeDetail = async (req, res) => {
const { mainCode, subCode } = req.params;
const client = await pool.connect();
try {
const query = `
DELETE FROM ver4.code_detail
WHERE main_code = $1 AND sub_code = $2
RETURNING *;
`;
const result = await client.query(query, [mainCode, subCode]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'deleteCodeDetail', req.user?.user_id, userIp, [
`Master code: ${mainCode}`,
`Sub code: ${subCode}`
]);
res.status(200).json({ message: "소분류 코드가 삭제되었습니다." });
} catch (err) {
console.error("deleteCodeDetail Error:", err);
res.status(500).json({ error: "소분류 삭제 실패" });
} finally {
client.release();
}
};
File diff suppressed because it is too large Load Diff
+448
View File
@@ -0,0 +1,448 @@
const passport = require('passport');
const pool = require('../db/pool.js');
const env = process.env.NODE_ENV;
const tbProject = env === 'production' ? 'tb_project' : '_test_tb_project';
const tbPermission = env === 'production' ? 'tb_permission' : '_test_tb_permission';
//////// pm-bcmf 연결용 테스트 코드 - pm-bcmf url에 담겨있던 쿼리 정보 저장, 조회
let bcmfId, startPath;
exports.setBcmfUrlQuery = async (req,res)=>{
bcmfId = req.body.id;
startPath = req.body.startPath;
}
exports.getBcmfUrlQuery = async (req,res)=>{
res.status(200).json({
bcmfId: bcmfId,
startPath: startPath
});
}
exports.getBcmfId = () => {
return bcmfId;
}
// passport.initialize()와 passport.session()를 추가해야 req.isAuthenticated() 함수를 사용할 수 있음.
exports.isLoggedIn = async(req,res,next)=>{
if (req.isAuthenticated()) { // 패스포트를 통해 로그인 했는지를 확인
console.log('🚥 [authController] isLoggedIn? : ture');
next();
} else {
// 1) 주소를 요청: http://localhost:3000/zip/status/?url=a.glb&url=b.glb
// 2) 로그인 페이지(html)로 이동(쿼리스트링으로 path에 주소 넣음): http://localhost:3000/login?path=/zip/status/?url=a.glb&url=b.glb
// 3) 로그인 이후 원래 주소로 이동: http://localhost:3000/zip/status/?url=a.glb&url=b.glb
console.log('🚥 [authController] isLoggedIn? : false');
res.redirect('/user/login?path='+req.originalUrl);//=> 로그인X인경우 이동할 페이지
}
}
exports.isNotLoggedIn = async(req,res,next)=>{
if(!req.isAuthenticated()){
console.log('🚥 [authController] isNotLoggedIn? : true');
next();
}else{
console.log('🚥 [authController] isNotLoggedIn? : false');
// const html = `
// <html>
// <body>
// <h3>로그인을 이미 했습니다</h3>
// <a class="btn-logout" href="/auth/logout">로그아웃</a>
// <br>
// <a href="/">index 화면으로 이동</a>
// </body>
// </html>
// `;
// res.send(html);
// res.status(200).json({redirect : req.})
next();//로그인 상태에서 로그인 요청 시 그냥 로그인 절차 밟도록 수정
}
}
exports.login =async(req,res,next)=>{
//진입 시도페이지로 다시 보내기위한 쿼리
const newQuery = req.query.path ? decodeURIComponent(req.query.path) : '/';
console.log('🌏 [authController.js] newQuery :', newQuery);
//서비스 이름 넣기
// req.body.service = 'PM_ver4';
req.body.service = process.env.SERVICE_NAME +' - ' + req.headers['origin'];
// 접근 아이피 넣기
/**
* cf-connecting-ip: Cloudflare가 원본 클라이언트 IP를 전달하는 헤더
* x-forwarded-for: 일반적인 프록시 체인 헤더
* req.ip: Express가 계산한 IP (trust proxy 설정 시 의미 있음)
* remoteAddress: 실제 TCP 연결의 IP
*/
req.body.user_ip = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (req.body.startPath) req.session.startPath = req.body.startPath;
passport.authenticate('local', (authError, user, info)=>{
if(authError){
console.error(authError);
return next(authError);
}
if(!user){
return res.json({error:info.message});
}
return req.login(user, async (loginError)=>{
//로그인 후 행위등록 - ex log추가...
if(loginError){
console.error(loginError);
return next(loginError);
}
return res.json({redirect:newQuery, user:user});
});
}) (req,res,next);
}
exports.logout = async (req,res,next)=>{
//////// pm-bcmf 연결용 테스트 코드 - 로그아웃할 때 bcmfId, startPath 초기화
bcmfId = undefined;
startPath = undefined;
req.logout(()=>{
//기본페이지로 redirect
// res.redirect('/user/login');
res.redirect('/user/login?path=/');
})
}
//로그인 상태 확인
exports.status = async(req,res,next)=>{
if(req.user){
const client = await pool.connect();
try{
if(!req.user.permission || req.user.permission ==null){
let {project_id} = req.query;
if (project_id && project_id !== 'undefined' && project_id !== 'null') {
let queryString = `
select
case
when exists (select 1 from ver4.${tbProject} where project_id = '${project_id}' and user_id = '${req.user.user_id}')
then 255
else (
select lev
from ver4.${tbPermission}
where project_id = '${project_id}' and user_id = '${req.user.user_id}'
)
end as lev
`;
let {rows} = await client.query(queryString);
req.user.permission = rows[0]?.lev || null;
} else {
req.user.permission = null;
}
}
let queryString2 = `select bookmark from ver4.tb_user where user_id = '${req.user.user_id}'`;
let bookmarkRes = await client.query(queryString2);
req.user.bookmark = bookmarkRes.rows[0]?.bookmark || '';
req.user.user_pw = undefined;
res.json({loggedIn : true, user : req.user});
}catch(err){
console.error('🚨 [authController.status] Error:', err);
res.status(500).json({loggedIn : false, error: err.message});
}finally{
client.release();
}
}else{
res.json({loggedIn : false});
}
}
//권한 설정 관련
// 권한 부여용 멤버 가져오기
exports.getMemberList = async (req,res,next)=>{
let {project_id} = req.query;
const client = await pool.connect();
try{
let queryString = `select user_id, user_nm, company, dept, position from ver4.tb_user
where ("group" is null or "group" = '') and is_resigned = false
order by user_nm asc`;
let {rows} = await client.query(queryString);
let permissionQuery = `select user_id, CASE lev WHEN 191 THEN 'sub-master' WHEN 15 THEN 'security-worker' WHEN 7 THEN 'worker' WHEN 3 THEN 'uploader' WHEN 1 THEN 'viewer' END as lev from ver4.${tbPermission} where project_id = $1`;
let permissionRes = await client.query(permissionQuery, [project_id]);
res.status(200).json({all : rows, permission : permissionRes.rows});
}catch(err){
console.error(err);
}finally{
client.release();
}
}
//권한 수정(json Array 로 받아서 parse후 처리)
/* project_id : 단일, a_per, target_id, target_name : 배열 */
exports.upsertPermission = async(req, res, next)=>{
let {project_id , targetArr, userInfoString } = req.body;
// let actor_user_id = req.user.user_id;
// let user_nm = req.user.user_nm;
// let activity_id = 'addUserPermission';
// let dateNow = makePostgresTimestamp(Date.now());
let userIp = req.ip;
const client = await pool.connect();
try{
let queryString = `insert into ver4.${tbPermission} (user_id, project_id, lev) values `;
for(let i = 0; i < targetArr.length; i++){
queryString += ` ('${targetArr[i].user_id}','${project_id}',${getPermissionLev(targetArr[i].lev)})${(i==targetArr.length-1)?'':','} `;
}
queryString += `on conflict (user_id, project_id) do update set
lev = EXCLUDED.lev
RETURNING *`;
let upsertRes = await client.query(queryString);
if(upsertRes.rows.length == targetArr.length){// 권한 부여 성공
// 유저권한 renderlog code
let group = {};
for (const user of targetArr){
if(!group[user.lev]) group[user.lev] = {lev: user.lev, before: user.before , userIds: [], names : []}
group[user.lev].userIds.push(user.user_id);
group[user.lev].names.push(user.user_nm);
}
const permissionArr = Object.values(group);
const levMap = { 'sub-master' : 'addPermission_subMaster', 'security-worker' : 'addPermission_securityWorker'};
// 로그를 담기 위한 배열
const logs = [];
for (const p of permissionArr){
let activity = levMap[p.lev] || `addPermission_${p.lev}`
let params = { projectId : project_id, activity, userInfoString, userIp, resourcePathArr: p.names, dataIdArr: p.userIds}
logs.push(params)
};
// upsert success와 함께 logs 배열 res
res.status(200).json({message : 'upsert success', logs});
}else{
res.status(500).json({message : 'upsert error'});
}
}catch(err){
console.error(err);
res.status(500).json({message : 'upsert error'});
}finally{
client.release();
}
}
//권한 삭제
exports.deletePermission = async(req, res, next)=>{
let {project_id, targetArr, userInfoString} = req.body;
let userIp = req.ip;
const client = await pool.connect();
try{
const conditions = targetArr.map((item, index) => {
return `(project_id = $1 AND user_id = $${index + 2})`;
}).join(' OR ');
let queryString = `DELETE FROM ver4.${tbPermission} WHERE ${conditions} RETURNING *`;
const params = [project_id, ...targetArr.map(item => item.user_id)];
let deleteRes = await client.query(queryString, params);
if(deleteRes.rows.length == targetArr.length){ //delete 성공
// 유저권한 renderlog code
let group = {};
for(const user of targetArr){
// 권한 삭제 경우 기존 권한 before를 바탕으로 로그에 넣어준다.
if(!group[user.before]) group[user.before] = {lev: user.before, before: user.lev, userIds: [], names: []};
group[user.before].userIds.push(user.user_id);
group[user.before].names.push(user.user_nm);
}
const permissionArr = Object.values(group);
const levMap = { 'sub-master' : 'deletePermission_subMaster', 'security-worker' : 'deletePermission_securityWorker'}
// 로그를 담기 위한 배열
const logs = [];
for (const p of permissionArr){
let activity = levMap[p.lev] || `deletePermission_${p.lev}`
let params = {projectId : project_id , activity, userInfoString, userIp, resourcePathArr : p.names, dataIdArr : p.userIds}
logs.push(params);
};
// delete success와 함께 logs 배열 res
res.status(200).json({message : 'delete success', logs});
}else{//delete 실패
res.status(500).json({message : 'delete error'});
}
}catch(err){
console.error(err);
}finally{
client.release();
}
}
// 회사-부서 목록
exports.getDeptList = async (req, res, next) => {
const company = req.query.company;
const client = await pool.connect();
try {
let queryString = 'SELECT DISTINCT dept FROM ver4.tb_user WHERE company = $1 AND is_resigned = false';
let queryResult = await client.query(queryString, [company]);
res.status(200).json({message : '200', result : queryResult.rows})
} catch(err) {
console.error('getCompanyList Error : ', err);
} finally {
client.release();
}
}
// 회사-부서-유저 목록
exports.getUserList = async (req, res, next) => {
const company = req.query.company;
const dept = req.query.dept;
const client = await pool.connect();
try {
let queryString = `SELECT user_id, user_nm, company, dept, position, is_resigned
FROM ver4.tb_user
WHERE company = $1
AND dept = $2
AND is_resigned = false
ORDER BY
CASE
WHEN position = '회장' THEN 1
WHEN position = '부회장' THEN 2
WHEN position = '사장' THEN 3
WHEN position = '상임고문' THEN 4
WHEN position = '기술위원' THEN 5
WHEN position = '부사장' THEN 6
WHEN position = '고문' THEN 7
WHEN position = '전무이사' THEN 8
WHEN position = '수석연구원' THEN 9
WHEN position = '상무이사' THEN 10
WHEN position = '이사' THEN 11
WHEN position = '책임연구원' THEN 12
WHEN position = '부장' THEN 13
WHEN position = '차장' THEN 14
WHEN position = '선임연구원' THEN 15
WHEN position = '과장' THEN 16
WHEN position = '연구원' THEN 17
WHEN position = '대리' THEN 18
WHEN position = '사원' THEN 19
ELSE 99
END,
user_nm ASC`;
let queryResult = await client.query(queryString, [company, dept]);
res.status(200).json({message : '200', result : queryResult.rows});
} catch(err) {
console.error('getUserList Error : ', err);
} finally {
client.release();
}
}
exports.getPermissionUserInfo = async (req, res, next) => {
const permission = req.body.permission;
const userIds = permission.map(p => p.user_id).filter(user_id => !user_id.includes('dev')).filter(user_id => !user_id.includes('SAVANNAH')).filter(user_id => !user_id.includes('ECHO')).filter(user_id => !user_id.includes('VOID')).filter(user_id => !user_id.includes('STRIKE')).filter(user_id => !user_id.includes('CHILL')).filter(user_id => !user_id.includes('NOIR')).filter(user_id => !user_id.includes('LESSER'));
const client = await pool.connect();
try{
let queryString = `SELECT user_id, user_nm, company, dept, position, is_resigned
FROM ver4.tb_user
WHERE user_id = ANY($1::text[]) AND is_resigned = false
ORDER BY
CASE
WHEN position = '회장' THEN 1
WHEN position = '부회장' THEN 2
WHEN position = '사장' THEN 3
WHEN position = '상임고문' THEN 4
WHEN position = '기술위원' THEN 5
WHEN position = '부사장' THEN 6
WHEN position = '고문' THEN 7
WHEN position = '전무이사' THEN 8
WHEN position = '수석연구원' THEN 9
WHEN position = '상무이사' THEN 10
WHEN position = '이사' THEN 11
WHEN position = '책임연구원' THEN 12
WHEN position = '부장' THEN 13
WHEN position = '차장' THEN 14
WHEN position = '선임연구원' THEN 15
WHEN position = '과장' THEN 16
WHEN position = '연구원' THEN 17
WHEN position = '대리' THEN 18
WHEN position = '사원' THEN 19
ELSE 99
END,
user_nm ASC`;
let queryResult = await client.query(queryString, [userIds]);
res.status(200).json({message : '200', result : queryResult.rows});
} catch(err) {
console.error('getPermissionUserInfo Error : ')
} finally {
client.release();
}
}
//내부용 함수
// 날짜 formatter
function formatDate(date){
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}. ${month}. ${day}. ${hours}:${minutes}:${seconds}`;
}
// 권한 lev string=>number
function getPermissionLev(lev){
let result;
switch(lev){
case 'sub-master':
result = 191;
break;
case 'security-worker':
result = 15;
break;
case 'worker':
result = 7;
break;
// case 'uploader':
// result = 3;
// break;
case 'viewer':
result = 1;
break;
}
return result;
}
// 권한 lev number => string (우선 select에서 case when then으로 처리)
function getPermissionName(lev){
let result;
switch(lev){
case 191:
result = 'sub-master';
break;
case 15:
result = 'security-worker';
break;
case 7:
result = 'worker';
break;
// case 3:
// result = 'uploader';
// break;
case 1:
result = 'viewer';
break;
}
return result;
}
+40
View File
@@ -0,0 +1,40 @@
// controllers/monitorController.js
const { createBullBoard } = require('@bull-board/api');
const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter.js');
const { ExpressAdapter } = require('@bull-board/express');
const { Queue } = require('bullmq');
const { redisConnection } = require('../config/redis.js');
// 1) 모니터링할 큐 인스턴스화
const convertQueue = new Queue('convert-pdf', { connection: redisConnection });
const convertQueue2 = new Queue('pdf-thumb', { connection: redisConnection });
const convertQueue3 = new Queue('zip-folder', { connection: redisConnection });
const convertQueue4 = new Queue('post-process-video', { connection: redisConnection });
const convertQueue5 = new Queue('ai-summarize', { connection: redisConnection });
const convertQueue6 = new Queue('api-summarize', {connection: redisConnection});
// const convertQueue3 = new Queue('test-job', { connection: redisConnection });
// 2) ExpressAdapter 생성 및 기본 경로 설정
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/admin/queues');
// 3) Bull-Board 생성
createBullBoard({
queues: [
new BullMQAdapter(convertQueue),
new BullMQAdapter(convertQueue2),
new BullMQAdapter(convertQueue3),
new BullMQAdapter(convertQueue4),
new BullMQAdapter(convertQueue5),
new BullMQAdapter(convertQueue6),
// new BullMQAdapter(convertQueue3),
// 여기에 더 필요한 큐 어댑터를 추가할 수 있습니다.
],
serverAdapter,
});
// 4) 실제 대시보드를 처리하는 Router 추출
const bullBoardController = serverAdapter.getRouter();
module.exports = bullBoardController;
+320
View File
@@ -0,0 +1,320 @@
const pool = require('../db/pool.js');
const env = process.env.NODE_ENV;
const { getIo } = require('../socket.js');
const tbProject = env == 'production'? 'tb_project':'_test_tb_project';
const tbPermission = env == 'production'? 'tb_permission':'_test_tb_permission';
function snakeToCamel(snakeStr) {
return snakeStr.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
exports.getEnvData = async (req,res,next)=>{
const deploymentType = process.env.DEPLOYMENT_TYPE;
const cloudType = process.env.CLOUD_TYPE;
const serviceName = process.env.SERVICE_NAME;
res.status(200).json({
message : 'getEnvData',
deploymentType: deploymentType,
cloudType: cloudType,
serviceName: serviceName,
});
}
exports.getProject = async (req,res,next)=>{
let {project_id} = req.query;
const client = await pool.connect();
//////// 원본코드
// let user_id = req.user.user_id;
// let user_group = req.user.group;
//////// pm-bcmf 연결용 테스트 코드 - pm-bcmf url로 접속하면 정상적으로 로그인이 되지 않아 req.user가 없기 때문에 user_id, user_group 강제 설정
let user_id = (req.user) ? req.user.user_id : undefined;
let user_group = (req.user) ? req.user.group : undefined;
const authController = require('../controllers/authController');
let bcmfId = await authController.getBcmfId();
if (bcmfId && bcmfId.includes('bcmf-')) {
user_id = bcmfId;
user_group = 'bcmf';
}
try {
// 테스트
let queryString = `
select p.*, u.*
from ver4.${tbProject} p
inner join ver4.tb_user u
on p.user_id = u.user_id
where show_in_index = true
`;
// let queryString = `
// select p.*, u.*
// from ver4.${tbProject} p
// inner join ver4.tb_user u
// on p.user_id = u.user_id
// where show_in_index = true
// `;
if(!user_group){
queryString += `and ( p.user_id = '${user_id}' or p.project_id in (select project_id from ver4.${tbPermission} where user_id = '${user_id}')) `;
}
// 251223 dev계정도 총괄 제한
if(user_group != 'super'){
queryString += ` and ( (p.project_type != 'secret' or p.project_type is null )
or exists (select 1 from ver4.${tbPermission} tp where tp.project_id = p.project_id and tp.user_id = '${user_id}')
) `;
}
if(project_id && project_id != '')
queryString += `and project_id = '${project_id}' `;
queryString += `order by p.create_date`;
console.log(queryString);
const {rows} = await client.query(queryString);
res.status(200).json({message : 'getProject', data: rows});
} catch(err) {
console.error(err);
res.status(500).json({message : 'getProject error'});
} finally {
client.release();
}
}
exports.mgmtFunc_updateProject = async (req,res)=>{
let { params } = req.body;
let { projectIdList, targetColumn, state, text } = params;
let value;
if (targetColumn == 'is_active') value = state;
if (targetColumn == 'banner_notice') value = text;
let projectArr = JSON.parse(projectIdList);
const client = await pool.connect();
try {
await client.query('BEGIN');
let updateQueryString = `
UPDATE ver4.${tbProject}
SET ${targetColumn} = $1
WHERE project_id = ANY($2)
`;
let selectQueryString = `
SELECT p.*, u.*
FROM ver4.${tbProject} p
INNER JOIN ver4.tb_user u
ON p.user_id = u.user_id
WHERE show_in_index = true;
`;
await client.query(updateQueryString, [value, projectArr]);
let { rows } = await client.query(selectQueryString);
await client.query('COMMIT');
params.allProject = rows;
let successMessage = `updateProject_${snakeToCamel(targetColumn)}_success`;
let io = getIo();
io.emit(successMessage, params);
res.status(200).json({
message: successMessage,
});
} catch(err) {
console.error("updateProject err:", err);
} finally {
client.release();
}
}
exports.getVersion = async(req,res,next)=>{
res.status(200).json({
message : 'getVersion success',
version : process.env.PROJECT_VERSION
});
}
// 🔻🔻🔻🔻🔻🔻🔻🔻 LIST화면용 함수 시작 🔻🔻🔻🔻🔻🔻🔻🔻
//프로젝트 (권한 있는) 목록들 가져오기
exports.getProjects = async (req,res,next) => {
let user_id = req.user.user_id;
const client = await pool.connect();
try{
let queryString = `select project_id, user_id, category, project_nm, create_date, lon, lat, height, step, emp_map, flyto from ver4.${tbProject} where project_id in (select project_id from ver4.${tbPermission} where user_id = $1)`;
const {rows} = await client.query(queryString, [user_id]);
res.status(200).json({message : 'getProjects', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'getProjects error'});
}finally{
client.release();
}
}
//북마크 프로젝트들 가져오기
exports.getBookmark = async (req,res,next) =>{
let user_id = req.user.user_id;
const client = await pool.connect();
try{
let queryString = `select project_id, user_id, category, project_nm, create_date, lon, lat, height, step, emp_map, flyto from ver4.tb_project where project_id in (select unnest(string_to_array(bookmark, ',')) from ver4.tb_user where user_id = $1)`;
const {rows} = await client.query(queryString, [user_id]);
res.status(200).json({message : 'getBookmark', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'getBookmark error'});
}finally{
client.release();
}
}
//북마크 update (JS의 array.toString() 형태로 넣기.)
exports.updateBookmark = async (req, res, next) =>{
let user_id = req.user.user_id;
const {bookmark} = req.query;
const client = await pool.connect();
try {
let queryString = `update ver4.tb_user set bookmark = $1 where user_id = $2`;
await client.query(queryString, [bookmark, user_id]);
res.status(200).json({message : `updateBookmark`});
}catch(err){
console.error(err);
res.status(500).json({message : `updateBookmark error`});
}finally{
client.release();
}
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 LIST화면용 함수 끝 🔺🔺🔺🔺🔺🔺🔺🔺
// 프로젝트 정보 업데이트
exports.updateProjectInfo = async (req, res) => {
let { params } = req.body;
let result = await updateProjectInfoAction(params);
res.status(200).json(result);
}
async function updateProjectInfoAction(params) {
const client = await pool.connect();
try {
const {
projectId,
category,
project_nm,
project_type,
step,
} = params
// let queryString;
// if(category !== 'overseas' && category !== 'bimproject') {
// queryString = `
// UPDATE ver4.${tbProject}
// SET
// project_type = $1,
// step = $2,
// project_nm = $3
// WHERE project_id = $4
// AND category = $5
// `;
// } else {
let queryString = `
UPDATE ver4.${tbProject}
SET
project_type = $1,
step = $2,
short_nm = $3
WHERE project_id = $4
AND category = $5
`;
// }
let values = [project_type, step, project_nm, projectId, category];
let { rows } = await client.query(queryString, values);
return { message: 'updateProjectInfo_success'};
} catch(err) {
console.error('updateProjectInfoAction err : ', err);
} finally {
client.release();
}
}
// 프로젝트 위치 업데이트
exports.updateLocationInfo = async (req, res) => {
let { params } = req.body;
let result = await updateLocationInfoAction(params);
res.status(200).json(result);
}
async function updateLocationInfoAction(params) {
const client = await pool.connect();
try {
const {
projectId,
category,
project_nm,
lon,
lat,
} = params
let queryString = `
UPDATE ver4.${tbProject}
SET
lon = $1,
lat = $2
WHERE project_id = $3
AND category = $4
AND project_nm = $5
RETURNING lon, lat;
`;
let values = [lon, lat, projectId, category, project_nm];
let { rows } = await client.query(queryString, values);
return { message: 'updateLocationInfo_success', data: rows[0] };
} catch(err) {
console.error('updateLocationInfoAction err : ', err);
} finally {
client.release();
}
}
// 일반 사용자용 보존/삭제 정책 조회 API
exports.getSystemPolicyPublic = async (req, res, next) => {
const client = await pool.connect();
try {
const result = await client.query("SELECT limit_file_count, limit_days, is_active FROM ver4.tb_system_policy WHERE policy_key = 'GLOBAL_DELETE_POLICY'");
if (result.rows.length === 0) {
return res.status(200).json({ limit_file_count: 100, limit_days: 30, is_active: false });
}
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("getSystemPolicyPublic error:", err);
res.status(500).json({ message: "정책 조회 중 오류가 발생했습니다." });
} finally {
client.release();
}
};
+295
View File
@@ -0,0 +1,295 @@
const pool = require('../db/pool.js');
const env = process.env.NODE_ENV;
const tbProject = env === 'production' ? 'tb_project' : '_test_tb_project';
const tbPermission = env === 'production' ? 'tb_permission' : '_test_tb_permission';
const cloudClient = require('../config/cloudClient.js');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const {
ListObjectsV2Command,
DeleteObjectCommand,
CopyObjectCommand,
HeadObjectCommand,
PutObjectCommand,
GetObjectCommand,
ListBucketsCommand
} = require('@aws-sdk/client-s3');
// 🔻🔻🔻🔻🔻🔻🔻🔻 리스트관련 함수 시작 🔻🔻🔻🔻🔻🔻🔻🔻
// 분류 가져오기
exports.getListClass = async(req, res, next) =>{
const {steps, search} = req.query;
const client = await pool.connect();
let user_id = req.user.user_id;
let user_group = req.user.group;
try{
let queryString = `select b.class ,b.large_class, b.mid_class from ver4.${tbProject} a, ver4.ref_project_class b
where a.class = b.class and a.show_in_index = true `;
if(!user_group){
queryString += `and ( a.user_id = '${user_id}' or a.project_id in (select project_id from ver4.${tbPermission} where user_id = '${user_id}')) `;
}
if(steps){
//입력되는 필더 조건에 따라 변경예정
queryString += `and a.step in ( `;
for(let i =0; i < steps.length; i++){
queryString += ` '${steps[i]}' ${(i < steps.length-1)?',':''}`
}
queryString += ` ) `;
}
if(search != '' && search != null && search != undefined){
queryString += ` and UPPER(a.project_nm) like '%${search}%' `;
}
queryString += ` order by b.class asc`;
const {rows} = await client.query(queryString);
//순서때문에 client에서 받아서 처리
res.status(200).json({data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'getDepth1 error'});
}finally{
client.release();
}
}
//depth3 list불러오기
exports.getList = async (req,res,next) =>{
const {steps, classNo, search } = req.query;
const client = await pool.connect();
let user_id = req.user.user_id;
let user_group = req.user.group;
try{
// let queryString = `select * from ver4.tb_project where project_id in (select unnest(string_to_array(bookmark, ',')) from ver4.tb_user where user_id = $1)`;
let queryString = `select a.project_id, a.category, a.project_nm, a.lon, a.lat, a.height, a.step, a.emp_map, a.flyto, b.user_id, b.user_nm, b.company, b.dept, a.class, b.position
from ver4.${tbProject} a, ver4.tb_user b where a.user_id = b.user_id and a.show_in_index = true and a.class = ${classNo} `;
if(steps){
//입력되는 필더 조건에 따라 변경예정
queryString += `and a.step in ( `;
for(let i =0; i < steps.length; i++){
queryString += ` '${steps[i]}' ${(i < steps.length-1)?',':''}`
}
queryString += ` ) `;
}
if(!user_group){
queryString += `and ( a.user_id = '${user_id}' or a.project_id in (select project_id from ver4.${tbPermission} where user_id = '${user_id}')) `;
}
if(search != '' && search != null && search != undefined){
queryString += ` and UPPER(a.project_nm) like '%${search}%' `;
}
queryString += `order by project_id, project_nm asc`;
const {rows} = await client.query(queryString);
res.status(200).json({message : 'getModelList', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'getModelList error'});
}finally{
client.release();
}
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 리스트관련 함수 끝 🔺🔺🔺🔺🔺🔺🔺🔺
// 🔻🔻🔻🔻🔻🔻🔻🔻 북마크관련 함수 시작 🔻🔻🔻🔻🔻🔻🔻🔻
exports.updateBookmark = async(req,res,next)=>{
const {bookmark} = req.query;
let user_id = req.user.user_id;
const client = await pool.connect();
try {
let queryString = `update ver4.tb_user set bookmark = $1 where user_id = $2`;
const { rows } = await client.query(queryString, [bookmark,user_id]);
res.status(200).json({ message: 'updateBookmark Done'});
}catch(error) {
console.error("updateBookmark err:", error);
}finally {
client.release();
}
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 북마크관련 함수 끝 🔺🔺🔺🔺🔺🔺🔺🔺
exports.projectstatusList = async(req,res,next)=>{
const client = await pool.connect();
try{
let queryString = `select * from ver4.test_tb_projectstatus order by create_date`;
const {rows} = await client.query(queryString);
res.status(200).json({message : 'projectstatusList', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'projectstatusList error'});
}finally{
client.release();
}
}
exports.getPresignedUrl = async(req, res, next) =>{
const {bucket, objectKey} = req.query;
try{
let command = new GetObjectCommand({
Bucket: bucket,
Key: objectKey,
});
//cloudClient에 강제로 던지기
let url = await getSignedUrl(cloudClient, command, { expiresIn: 60 * 30 }); // 30분 유효
res.status(200).json({message : 'getProsignedUrl', data : url});
}catch(err){
console.error(err);
res.status(500).json({message : 'getProsignedUrl error'});
}
}
exports.uploadUrl = async(req, res, next) => {
const {filename} = req.query;
try{
const command = new PutObjectCommand({
Bucket: 'gsimdev',
Key: `projectStatus/${filename}`,
ContentType: 'application/pdf'
});
//cloudClient에 강제로 던지기
let url = await getSignedUrl(cloudClient, command, { expiresIn: 60 * 30 }); // 30분 유효
res.status(200).json({message : 'uploadUrl', data : url});
}catch(err){
console.error(err);
res.status(500).json({message : 'uploadUrl error'});
}
}
exports.insertProjectStatusData = async(req, res, next) => {
const {filename} = req.query;
const client = await pool.connect();
try{
let queryString = `insert into ver4.test_tb_projectstatus (file_nm, object_key, bucket) values ($1, $2, $3) returning *`;
const {rows} = await client.query(queryString, [filename, `projectStatus/${filename}`, 'gsimdev']);
res.status(200).json({message : 'insertProjectStatusData', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'insertProjectStatusData error'});
}finally{
client.release();
}
}
//우선 DB만 삭제
exports.deleteFile = async(req, res, next) => {
const {objectKey, bucket} = req.query;
const client = await pool.connect();
try{
let queryString = `delete from ver4.test_tb_projectstatus where object_key = $1 and bucket = $2 returning *`;
const {rows} = await client.query(queryString, [objectKey, bucket]);
res.status(200).json({message : 'deleteFile', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'deleteFile error'});
}finally{
client.release();
}
}
// 🔻🔻🔻🔻🔻🔻🔻🔻 new! 리스트관련 함수 시작 🔻🔻🔻🔻🔻🔻🔻🔻
// 전체 리스트 가져오기
exports.getAllList = async(req, res, next) =>{
const {steps, search, type, category} = req.query;
const client = await pool.connect();
let user_id = req.user.user_id;
let user_group = req.user.group;
try{
let queryString = `select b.class ,b.large_class, b.mid_class, a.*, (select user_nm from ver4.tb_user where user_id = a.user_id) as master from ver4.${tbProject} a, ver4.ref_project_class b
where a.class = b.class and a.show_in_index = true and category = '${category}' `;
if(!user_group){
queryString += `and ( a.user_id = '${user_id}' or a.project_id in (select project_id from ver4.${tbPermission} where user_id = '${user_id}')) `;
}
if(steps && steps != 'overseas'){
//입력되는 필더 조건에 따라 변경
queryString += `and a.step in ( `;
for(let i =0; i < steps.length; i++){
queryString += ` '${steps[i]}' ${(i < steps.length-1)?',':''}`
}
queryString += ` ) `;
}
if(type && type != 'overseas'){
//입력되는 필더 조건에 따라 변경
queryString += `and a.project_type in ( `;
for(let i =0; i < type.length; i++){
queryString += ` '${type[i]}' ${(i < type.length-1)?',':''}`
}
queryString += ` ) `;
}
if(search != '' && search != null && search != undefined){
queryString += ` and UPPER(a.project_nm) like '%${search}%' `;
}
if(steps == 'overseas' || type == 'overseas'){
queryString += ` and a.category = 'overseas' `;
}
queryString += ` order by b.class asc`;
const {rows} = await client.query(queryString);
//순서때문에 client에서 받아서 처리
res.status(200).json({data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'getDepth1 error'});
}finally{
client.release();
}
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 new! 리스트관련 함수 끝 🔺🔺🔺🔺🔺🔺🔺🔺
exports.setProjectType = async (req,res, next)=>{
const {project_id, type} = req.query;
const client = await pool.connect();
try{
let query = `update ver4.tb_project set project_type = $1 where project_id = $2 returning *`;
const {rows} = await client.query(query, [type, project_id]);
res.status(200).json({message : 'setProjectTypeSuccess', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'setProjectType error'});
}finally{
client.release();
}
}
exports.setProjectStep = async (req,res, next)=>{
const {project_id, step} = req.query;
const client = await pool.connect();
try{
let query = `update ver4.tb_project set step = $1 where project_id = $2 returning *`;
const {rows} = await client.query(query, [step, project_id]);
res.status(200).json({message : 'setProjectStepSuccess', data : rows});
}catch(err){
console.error(err);
res.status(500).json({message : 'setProjectStep error'});
}finally{
client.release();
}
}
File diff suppressed because it is too large Load Diff
+710
View File
@@ -0,0 +1,710 @@
const path = require('path');
const pool = require('../db/pool.js');
const fs = require('fs');
const multer = require('multer');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const { PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const onPremiseClient = require('../config/onPremiseClient.js');
const cloudClient = require('../config/cloudClient.js');
const storageClients = {
'ONPREMISE': onPremiseClient,
'CLOUD': cloudClient
}
const deploymentType = process.env.DEPLOYMENT_TYPE;
const s3 = storageClients[deploymentType];
exports.getData = async (req, res, next) => {
const client = await pool.connect();
try {
let { projectId } = req.query;
let queryString = `
select project_id,
project_no,
business_purpose,
location_img,
continent,
performance_area,
reference_area,
overview_img,
facility_size_overview,
task_nm_kr,
task_nm_en,
task_purpose,
task_type,
client,
financial,
financial_country,
bid,
selection_method,
joint_contract_nm,
joint_contract_shareratio,
contract_amount,
foreign_currency_amount,
contract_date,
commencement_date,
original_completion_date,
completion_date,
projectmanager_nm,
manager_nm,
nation_nm,
client_origin,
support_department,
support_manager_nm,
representative_company,
order_size_krw,
order_size_usd,
scheuled_commencement_date,
contract_period,
abbreviated_name,
department,
data_size,
issue,
currency_code,
joint_contract,
lead_company,
nation_code,
nation_offset
from ver4.tb_overview
where project_id = $1;
`;
const result = await client.query(queryString, [projectId]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('getData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.getCalendarEventData = async (req, res) => {
const client = await pool.connect();
try {
let { nationName, projectId, currentYear, currentMonth } = req.query;
let queryString = `
SELECT
calendar_event_id,
project_id,
type,
title,
content,
color,
start_date,
end_date,
nation_nm
FROM ver4.tb_calendar_event
WHERE (
(type = 'holiday' AND nation_nm IN ('한국', $1)) OR
(type = 'schedule' AND project_id = $2)
)
AND EXTRACT(YEAR FROM TO_DATE(start_date, 'YYYY-MM-DD')) = $3
AND EXTRACT(MONTH FROM TO_DATE(start_date, 'YYYY-MM-DD')) = $4;
`
const result = await client.query(queryString, [nationName, projectId, currentYear, currentMonth]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('getCalendarEventData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message
});
} finally {
client.release();
}
}
exports.getTaskPeriodData = async (req, res) => {
const client = await pool.connect();
try {
let { projectId } = req.query;
let queryString = `
SELECT
task_history_id,
project_id,
task_order,
suspension_date,
suspension_reason,
resumption_date,
consultation_content,
change_date
FROM ver4.tb_task_history
WHERE project_id = $1;
`;
const result = await client.query(queryString, [projectId]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('getTaskPeriodData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.getFacilitySizeData = async (req, res) => {
const client = await pool.connect();
try {
let { projectId } = req.query;
let queryString = `
SELECT
facility_id,
project_id,
key,
value,
title
FROM ver4.tb_facility_size AS t
WHERE project_id = $1
ORDER BY
(SELECT MIN(facility_id)
FROM ver4.tb_facility_size
WHERE title = t.title),
facility_id;
`;
const result = await client.query(queryString, [projectId]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('getFacilitySizeData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.saveScheduleData = async (req, res) => {
const client = await pool.connect();
try {
let {scheduleId, projectId, title, content, color, startDateTimeStr, endDateTimeStr, country } = req.body;
const type = 'schedule';
let queryString;
let result;
if(scheduleId === undefined){
queryString = `
INSERT INTO ver4.tb_calendar_event (project_id, type, title, content, color, start_date, end_date, nation_nm)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
result = await client.query(queryString, [projectId, type, title, content, color, startDateTimeStr, endDateTimeStr, country]);
} else {
queryString = `
INSERT INTO ver4.tb_calendar_event (calendar_event_id, project_id, type, title, content, color, start_date, end_date, nation_nm)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (calendar_event_id) DO UPDATE SET
project_id = EXCLUDED.project_id,
type = EXCLUDED.type,
title = EXCLUDED.title,
content = EXCLUDED.content,
color = EXCLUDED.color,
start_date = EXCLUDED.start_date,
end_date = EXCLUDED.end_date,
nation_nm = EXCLUDED.nation_nm
`;
result = await client.query(queryString, [scheduleId, projectId, type, title, content, color, startDateTimeStr, endDateTimeStr, country]);
}
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('saveScheduleData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
}
exports.deleteScheduleData = async (req, res) => {
const client = await pool.connect();
try {
let { scheduleId } = req.body;
let queryString = `
DELETE FROM ver4.tb_calendar_event
WHERE calendar_event_id = $1;
`;
const result = await client.query(queryString, [scheduleId]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('deleteScheduleData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.deleteTaskPeriodData = async (req, res) => {
const client = await pool.connect();
try {
let { deleteArr } = req.body;
let queryString = `
DELETE FROM ver4.tb_task_history
WHERE task_history_id = ANY($1);
`;
const result = await client.query(queryString, [deleteArr]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('deleteTaskHistory error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.deleteSectionData = async (req, res) => {
const client = await pool.connect();
try {
let { title } = req.body;
let queryString = `
DELETE FROM ver4.tb_facility_size
WHERE title = $1;
`;
const result = await client.query(queryString, [title]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('deleteSectionData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.deleteCellData = async (req, res) => {
const client = await pool.connect();
try {
let { deleteArr } = req.body;
const ids = deleteArr.map(cell => cell.id);
let queryString = `
DELETE FROM ver4.tb_facility_size
WHERE facility_id = ANY($1);
`;
const result = await client.query(queryString, [ids]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('deleteCellData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.deleteLocationImgData = async (req, res) => {
const client = await pool.connect();
try {
let { projectId } = req.body;
let queryString = `
UPDATE ver4.tb_overview
SET location_img = ''
WHERE project_id = $1;
`;
const result = await client.query(queryString, [projectId]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('updateLocationImgData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.deleteOverviewImgData = async (req, res) => {
const client = await pool.connect();
try {
let { projectId } = req.body;
let queryString = `
UPDATE ver4.tb_overview
SET overview_img = ''
WHERE project_id = $1;
`;
const result = await client.query(queryString, [projectId]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('updateLocationImgData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
exports.saveSectionLeftData = async (req, res, next) => {
const client = await pool.connect();
try {
let {projectId, businessPurpose, continent, performanceArea, referenceArea, nation, facilityOverview, locationImgKey, originFileSize } = req.body;
let values = [projectId, businessPurpose, continent, performanceArea, referenceArea, nation, facilityOverview, locationImgKey, originFileSize];
let queryString = `
INSERT INTO ver4.tb_overview ( project_id, business_purpose, continent, performance_area, reference_area, nation_nm, facility_size_overview, location_img, data_size)
VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (project_id)
DO UPDATE SET business_purpose = EXCLUDED.business_purpose, location_img = EXCLUDED.location_img, continent = EXCLUDED.continent,performance_area = EXCLUDED.performance_area, reference_area = EXCLUDED.reference_area, data_size = EXCLUDED.data_size, nation_nm = EXCLUDED.nation_nm, facility_size_overview = EXCLUDED.facility_size_overview;
`;
const result = await client.query(queryString, values);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
next(error);
res.status(500).json({ success: false, message: '파일 업로드 중 오류가 발생했습니다.' });
} finally {
client.release();
}
}
exports.saveSectionMiddleData = async (req, res, next) => {
const client = await pool.connect();
try {
let {projectId, abbreviatedName, taskNmKr, taskNmEn, taskPurpose, orderSizeUsd, orderSizeKrw, scheduledCommencementDate, contractPeriod, clientOrigin, financial, financialCountry, selectionMethod, projectManagerNm, managerNm, supportDepartment, supportManagerNm, contractDate, commencementDate, originalCompletionDate, completionDate, projectNo, taskType, bid, relativeClient, department, jointContractComapnyName, jointContractShares, jointContractKrw, jointContractUsd, representativeCompany } = req.body;
let values = [projectId, abbreviatedName, taskNmKr, taskNmEn, taskPurpose, orderSizeUsd, orderSizeKrw, scheduledCommencementDate, contractPeriod, clientOrigin, financial, financialCountry, selectionMethod, projectManagerNm, managerNm, supportDepartment, supportManagerNm, contractDate, commencementDate, originalCompletionDate, completionDate, projectNo, taskType, bid, relativeClient, department, jointContractComapnyName, jointContractShares, jointContractKrw, jointContractUsd, representativeCompany];
let queryString = `
INSERT INTO ver4.tb_overview (
project_id, abbreviated_name, task_nm_kr, task_nm_en, task_purpose,
order_size_usd, order_size_krw, scheuled_commencement_date, contract_period,
client_origin, financial, financial_country, selection_method,
projectmanager_nm, manager_nm, support_department, support_manager_nm,
contract_date, commencement_date, original_completion_date, completion_date,
project_no, task_type, bid, client, department,
joint_contract_nm, joint_contract_shareratio, contract_amount, foreign_currency_amount, representative_company
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31
)
ON CONFLICT (project_id)
DO UPDATE SET
abbreviated_name = EXCLUDED.abbreviated_name,
task_nm_kr = EXCLUDED.task_nm_kr,
task_nm_en = EXCLUDED.task_nm_en,
task_purpose = EXCLUDED.task_purpose,
order_size_usd = EXCLUDED.order_size_usd,
order_size_krw = EXCLUDED.order_size_krw,
scheuled_commencement_date = EXCLUDED.scheuled_commencement_date,
contract_period = EXCLUDED.contract_period,
client_origin = EXCLUDED.client_origin,
financial = EXCLUDED.financial,
financial_country = EXCLUDED.financial_country,
selection_method = EXCLUDED.selection_method,
projectmanager_nm = EXCLUDED.projectmanager_nm,
manager_nm = EXCLUDED.manager_nm,
support_department = EXCLUDED.support_department,
support_manager_nm = EXCLUDED.support_manager_nm,
contract_date = EXCLUDED.contract_date,
commencement_date = EXCLUDED.commencement_date,
original_completion_date = EXCLUDED.original_completion_date,
completion_date = EXCLUDED.completion_date,
project_no = EXCLUDED.project_no,
task_type = EXCLUDED.task_type,
bid = EXCLUDED.bid,
client = EXCLUDED.client,
department = EXCLUDED.department,
joint_contract_nm = EXCLUDED.joint_contract_nm,
joint_contract_shareratio = EXCLUDED.joint_contract_shareratio,
contract_amount = EXCLUDED.contract_amount,
foreign_currency_amount = EXCLUDED.foreign_currency_amount,
representative_company = EXCLUDED.representative_company;
`
const result = await client.query(queryString, values);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
next(error);
res.status(500).json({ success: false, message: 'section2 Save Data Error' });
} finally {
client.release();
}
};
exports.saveTaskHistoryData = async (req, res, next) => {
const client = await pool.connect();
try {
for (let i = 0; i < req.body.length; i++) {
let { projectId, order, suspensionDate, suspensionReason, resumptionDate, consultationContent, changeDate } = req.body[i];
let queryString = `
INSERT INTO ver4.tb_task_history (project_id, task_order, suspension_date, suspension_reason, resumption_date, consultation_content, change_date)
VALUES ($1, $2, $3, $4, $5, $6, $7);
`
await client.query(queryString, [projectId, order, suspensionDate, suspensionReason, resumptionDate, consultationContent, changeDate]);
}
res.json({
success: true,
message: '200',
});
} catch (error) {
next(error);
res.status(500).json({ success: false, message: 'task history Save Data Error' });
} finally {
client.release();
}
};
exports.saveSectionLeftTabData = async (req, res, next) => {
const client = await pool.connect();
try {
const sections = req.body;
for (const title of Object.keys(sections)) {
const rows = sections[title];
for (let i = 0; i < rows.length; i++) {
const { key, value, id, projectId } = rows[i];
if (id == null || id === '') {
const queryStringInsert = `
INSERT INTO ver4.tb_facility_size (project_id, key, value, title)
VALUES ($1, $2, $3 , $4)
ON CONFLICT (facility_id)
DO UPDATE SET project_id = EXCLUDED.project_id, key = EXCLUDED.key, value = EXCLUDED.value, title = EXCLUDED.title;
`;
await client.query(queryStringInsert, [projectId, key, value, title]);
} else {
const queryStringUpdate = `
INSERT INTO ver4.tb_facility_size (project_id, key, value, facility_id, title)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (facility_id)
DO UPDATE SET project_id = EXCLUDED.project_id, key = EXCLUDED.key, value = EXCLUDED.value, title = EXCLUDED.title;
`;
await client.query(queryStringUpdate, [projectId, key, value, id, title]);
}
}
}
return res.json({ success: true, message: '200', });
} catch (error) {
next(error);
res.status(500).json({ success: false, message: 'FacilitySize Save Data Error' });
} finally {
client.release();
}
};
exports.saveIssueData = async (req, res, next) => {
const client = await pool.connect();
try {
let { projectId, issueData } = req.body;
let queryString = `
INSERT INTO ver4.tb_overview (
project_id, issue
) VALUES (
$1, $2
)
ON CONFLICT (project_id)
DO UPDATE SET
issue = EXCLUDED.issue
`
const result = await client.query(queryString, [projectId, issueData]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
next(error);
res.status(500).json({ success: false, message: 'Save Issue Data Error' });
} finally {
client.release();
}
};
exports.generateUploadImgUrl = async (req,res,next) => {
const projectId = req.baseUrl.split('/')[1];
let {fileName} = req.body;
let bucket = projectId;
let key = 'overview/' + fileName;
try{
// s3 명렁어 구성
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: 'application/octet-stream'
});
// presigned url 생성
const url = await getSignedUrl(s3, command, { expiresIn: 60 * 5});
// 클라이언트에 return
res.json({ url, key });
}catch(error){
console.error('UploadPresigned URL 생성 실패: ', error);
next(error);
}
}
exports.generateGetImgUrl = async (req,res,next) => {
const projectId = req.baseUrl.split('/')[1];
let { key } = req.query;
let bucket = projectId;
try{
// s3 명렁어 구성
const command = new GetObjectCommand({
Bucket : bucket,
Key : key
});
const url = await getSignedUrl(s3, command, {expiresIn: 60});
res.json({ url });
} catch(error){
console.error('GetPresigned URL 생성 실패: ',error);
next(error);
}
}
exports.generateDeleteImgUrl = async (req, res, next) => {
const projectId = req.baseUrl.split('/')[1];
let { key } = req.body;
let bucket = projectId;
try{
// s3 명령어 구성
const command = new DeleteObjectCommand({
Bucket : bucket,
Key : key
});
const url = await getSignedUrl(s3, command, {expiresIn: 60});
res.json({ url });
} catch(error) {
console.error('DeletePresigned URL 생성 실패: ',error);
next(error)
}
}
exports.updateOverviewImgData = async (req, res) => {
const client = await pool.connect();
try {
let { projectId, locationImgKey, originFileSize } = req.body;
let queryString = `
UPDATE ver4.tb_overview
SET location_img = $2, data_size = $3
WHERE project_id = $1;
`;
const result = await client.query(queryString, [projectId, locationImgKey, originFileSize]);
res.json({
success: true,
message: '200',
data: result.rows,
});
} catch (error) {
console.error('updateLocationImgData error: ', error);
res.status(500).json({
success: false,
message: '500',
error: error.message,
});
} finally {
client.release();
}
};
+31
View File
@@ -0,0 +1,31 @@
require('dotenv').config();
module.exports = {
"ONPREMISE_DB": {
"host" : process.env.ONPREMISE_POSTGRES_HOST,
"port" : parseInt(process.env.ONPREMISE_POSTGRES_PORT) || 5432,
"database" : process.env.ONPREMISE_POSTGRES_DATABASE,
"user" : process.env.ONPREMISE_POSTGRES_USER,
"password" : process.env.ONPREMISE_POSTGRES_PASSWORD,
"max": 40,
// "idleTimeoutMillis": 30000,
"idleTimeoutMillis": 5000,
"connectionTimeoutMillis": 5000, // 최대 커넥션 대기 시간
// "statement_timeout": 3000
},
"CLOUD_DB": {
"host" : process.env.CLOUD_POSTGRES_HOST,
"port" : parseInt(process.env.CLOUD_POSTGRES_PORT) || 5432,
"database" : process.env.CLOUD_POSTGRES_DATABASE,
"user" : process.env.CLOUD_POSTGRES_USER,
"password" : process.env.CLOUD_POSTGRES_PASSWORD,
"max": 40,
// "idleTimeoutMillis": 30000,
"idleTimeoutMillis": 5000,
"connectionTimeoutMillis": 5000, // 최대 커넥션 대기 시간
// "statement_timeout": 3000,
"ssl": {
ca: process.env.CLOUD_POSTGRES_SSL_CA
}
},
}
+14
View File
@@ -0,0 +1,14 @@
const pool = require('./pool.js');
require('dotenv').config();
module.exports = async () => {
let client;
try {
client = await pool.connect();
console.log(`📡 [db/index.js] ${process.env.DEPLOYMENT_TYPE} DB 연결 성공`);
} catch (err) {
console.error('📡 [db/index.js] DB 연결 에러:', err.stack);
} finally {
client.release(); // 연결 객체를 풀에 반환합니다.
}
};
+10
View File
@@ -0,0 +1,10 @@
const { Pool } = require('pg');
require('dotenv').config();
const env = `${process.env.DEPLOYMENT_TYPE}_DB`; // ONPREMISE_DB / CLOUD_DB
const config= require('./config.js')[env];
const pool = new Pool(config);
// 커넥션 설정: https://jojoldu.tistory.com/634
module.exports = pool;
+62
View File
@@ -0,0 +1,62 @@
const pool = require("./db/pool.js");
async function checkAndAddColumn(tableName, columnName, columnDefinition) {
const client = await pool.connect();
try {
// 컬럼 존재 여부 확인
const checkQuery = `
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = 'ver4'
AND table_name = $1
AND column_name = $2;
`;
const res = await client.query(checkQuery, [tableName, columnName]);
const exists = parseInt(res.rows[0].count) > 0;
if (!exists) {
console.log(`Adding column [${columnName}] to [ver4.${tableName}]...`);
const alterQuery = `ALTER TABLE ver4.${tableName} ADD COLUMN ${columnName} ${columnDefinition};`;
await client.query(alterQuery);
console.log(`Successfully added [${columnName}] to [ver4.${tableName}].`);
} else {
console.log(`Column [${columnName}] already exists in [ver4.${tableName}].`);
}
} catch (err) {
console.error(`Error processing table ${tableName}, column ${columnName}:`, err);
} finally {
client.release();
}
}
async function run() {
try {
console.log("Starting DB patch processing...");
// 1. tb_data
await checkAndAddColumn("tb_data", "popup_size", "BIGINT DEFAULT 0");
await checkAndAddColumn("tb_data", "preview_size", "BIGINT DEFAULT 0");
// 2. _test_tb_data
await checkAndAddColumn("_test_tb_data", "popup_size", "BIGINT DEFAULT 0");
await checkAndAddColumn("_test_tb_data", "preview_size", "BIGINT DEFAULT 0");
// 3. tb_official_doc_file
await checkAndAddColumn("tb_official_doc_file", "popup_size", "BIGINT DEFAULT 0");
await checkAndAddColumn("tb_official_doc_file", "preview_size", "BIGINT DEFAULT 0");
// 4. tb_download_folder
await checkAndAddColumn("tb_download_folder", "expire_date", "TIMESTAMP");
await checkAndAddColumn("tb_download_folder", "made", "BOOLEAN DEFAULT FALSE");
await checkAndAddColumn("tb_download_folder", "path", "TEXT");
await checkAndAddColumn("tb_download_folder", "name", "TEXT");
console.log("DB patch processing finished.");
} catch (err) {
console.error("Migration script failed:", err);
} finally {
await pool.end();
}
}
run();
+134
View File
@@ -0,0 +1,134 @@
const pool = require("./db/pool.js");
async function runPatch() {
const client = await pool.connect();
try {
console.log("🚀 Starting Admin Dashboard DB Patch...");
// 1. code_master 테이블 생성
console.log("Creating ver4.code_master table...");
await client.query(`
CREATE TABLE IF NOT EXISTS ver4.code_master (
main_code VARCHAR(30) PRIMARY KEY,
main_code_nm VARCHAR(100) NOT NULL,
use_yn CHAR(1) DEFAULT 'Y',
rmk VARCHAR(255)
);
`);
// 2. code_detail 테이블 생성
console.log("Creating ver4.code_detail table...");
await client.query(`
CREATE TABLE IF NOT EXISTS ver4.code_detail (
main_code VARCHAR(30) REFERENCES ver4.code_master(main_code) ON DELETE CASCADE,
sub_code VARCHAR(30) NOT NULL,
base_code VARCHAR(61) UNIQUE NOT NULL,
code_nm VARCHAR(100) NOT NULL,
sort_ord INT DEFAULT 1,
use_yn CHAR(1) DEFAULT 'Y',
rmk VARCHAR(255),
PRIMARY KEY (main_code, sub_code)
);
`);
// 3. tb_system_policy 테이블 생성
console.log("Creating ver4.tb_system_policy table...");
await client.query(`
CREATE TABLE IF NOT EXISTS ver4.tb_system_policy (
policy_id SERIAL PRIMARY KEY,
policy_key VARCHAR(50) UNIQUE NOT NULL,
limit_file_count INT DEFAULT 100,
limit_days INT DEFAULT 30,
is_active BOOLEAN DEFAULT FALSE,
upd_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
// 4. tb_banner_notice 테이블 생성
console.log("Creating ver4.tb_banner_notice table...");
await client.query(`
CREATE TABLE IF NOT EXISTS ver4.tb_banner_notice (
banner_id SERIAL PRIMARY KEY,
project_id VARCHAR(50) REFERENCES ver4.tb_project(project_id),
reg_date DATE DEFAULT CURRENT_DATE,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
notice_text TEXT NOT NULL,
status_code VARCHAR(61) REFERENCES ver4.code_detail(base_code)
);
`);
// 5. tb_auto_clean_log 테이블 생성
console.log("Creating ver4.tb_auto_clean_log table...");
await client.query(`
CREATE TABLE IF NOT EXISTS ver4.tb_auto_clean_log (
log_id SERIAL PRIMARY KEY,
clean_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
project_id VARCHAR(50) DEFAULT 'SYSTEM',
clean_path TEXT NOT NULL,
criteria_info VARCHAR(100),
result_status VARCHAR(20) NOT NULL
);
`);
console.log("Tables created successfully. Now Seeding data...");
// 6. code_master 기초 데이터 Seeding
const masterSeeds = [
{ main_code: 'PROJECT_CATEGORY', main_code_nm: '프로젝트 구분', use_yn: 'Y', rmk: '현장 구분 대분류 코드' },
{ main_code: 'USER_GROUP', main_code_nm: '사용자 권한그룹', use_yn: 'Y', rmk: '어드민 및 유저 권한그룹 대분류' },
{ main_code: 'NOTICE_STATUS', main_code_nm: '배너 송출상태', use_yn: 'Y', rmk: '실시간 배너 송출 상태 대분류' }
];
for (const seed of masterSeeds) {
await client.query(`
INSERT INTO ver4.code_master (main_code, main_code_nm, use_yn, rmk)
VALUES ($1, $2, $3, $4)
ON CONFLICT (main_code)
DO UPDATE SET main_code_nm = EXCLUDED.main_code_nm, use_yn = EXCLUDED.use_yn, rmk = EXCLUDED.rmk;
`, [seed.main_code, seed.main_code_nm, seed.use_yn, seed.rmk]);
}
// 7. code_detail 기초 데이터 Seeding
const detailSeeds = [
// 프로젝트 카테고리
{ main_code: 'PROJECT_CATEGORY', sub_code: 'tdc', base_code: 'PROJECT_CATEGORY_tdc', code_nm: 'TDC', sort_ord: 1 },
{ main_code: 'PROJECT_CATEGORY', sub_code: 'gpd', base_code: 'PROJECT_CATEGORY_gpd', code_nm: 'GPD', sort_ord: 2 },
{ main_code: 'PROJECT_CATEGORY', sub_code: 'bimproject', base_code: 'PROJECT_CATEGORY_bimproject', code_nm: 'BIM프로젝트', sort_ord: 3 },
{ main_code: 'PROJECT_CATEGORY', sub_code: 'overseas', base_code: 'PROJECT_CATEGORY_overseas', code_nm: '해외현장', sort_ord: 4 },
// 사용자 그룹
{ main_code: 'USER_GROUP', sub_code: 'super', base_code: 'USER_GROUP_super', code_nm: '수퍼관리자', sort_ord: 1 },
{ main_code: 'USER_GROUP', sub_code: 'dev', base_code: 'USER_GROUP_dev', code_nm: '개발자', sort_ord: 2 },
{ main_code: 'USER_GROUP', sub_code: 'general', base_code: 'USER_GROUP_general', code_nm: '일반사용자', sort_ord: 3 },
// 배너 상태
{ main_code: 'NOTICE_STATUS', sub_code: 'active', base_code: 'NOTICE_STATUS_active', code_nm: '송출중', sort_ord: 1 },
{ main_code: 'NOTICE_STATUS', sub_code: 'scheduled', base_code: 'NOTICE_STATUS_scheduled', code_nm: '예약됨', sort_ord: 2 },
{ main_code: 'NOTICE_STATUS', sub_code: 'expired', base_code: 'NOTICE_STATUS_expired', code_nm: '만료', sort_ord: 3 }
];
for (const seed of detailSeeds) {
await client.query(`
INSERT INTO ver4.code_detail (main_code, sub_code, base_code, code_nm, sort_ord)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (main_code, sub_code)
DO UPDATE SET base_code = EXCLUDED.base_code, code_nm = EXCLUDED.code_nm, sort_ord = EXCLUDED.sort_ord;
`, [seed.main_code, seed.sub_code, seed.base_code, seed.code_nm, seed.sort_ord]);
}
// 8. tb_system_policy 글로벌 공통 자동 삭제 정책 1건 Seeding
await client.query(`
INSERT INTO ver4.tb_system_policy (policy_key, limit_file_count, limit_days, is_active)
VALUES ('GLOBAL_DELETE_POLICY', 100, 30, FALSE)
ON CONFLICT (policy_key) DO NOTHING;
`);
console.log("🎉 Seeding completed successfully!");
} catch (err) {
console.error("❌ DB Patch Error:", err);
} finally {
client.release();
await pool.end();
}
}
runPatch();
+40
View File
@@ -0,0 +1,40 @@
version: '3.8'
services:
# 1. 데이터베이스 (PostgreSQL)
postgres:
image: postgres:15-alpine
container_name: pm-postgres
ports:
- "5432:5432"
environment:
POSTGRES_DB: pm_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: your_password
volumes:
- pgdata:/var/lib/postgresql/data
# 2. 작업 큐 백킹 스토어 (Redis)
redis:
image: redis:7-alpine
container_name: pm-redis
ports:
- "6379:6379"
# 3. 로컬 파일 스토리지 (MinIO)
minio:
image: minio/minio:latest
container_name: pm-minio
ports:
- "9000:9000" # API 포트
- "9001:9001" # 관리자 콘솔 웹 포트
environment:
MINIO_ROOT_USER: minio_access_key
MINIO_ROOT_PASSWORD: minio_secret_key
volumes:
- miniodata:/data
command: server /data --console-address ":9001"
volumes:
pgdata:
miniodata:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
/* 폰트 */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR&family=Protest+Riot&display=swap');
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css");
@font-face {
font-family: 'S-CoreDream-3Light';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_six@1.2/S-CoreDream-3Light.woff') format('woff');
font-style: normal;
}
/* 폰트적용 */
/* * { font-family: 'Noto Sans KR', 'Protest Riot', 'Gowun Dodum', sans-serif; } */
+10
View File
@@ -0,0 +1,10 @@
/* https://velog.io/@teo/2022-CSS-Reset-%EB%8B%A4%EC%8B%9C-%EC%8D%A8%EB%B3%B4%EA%B8%B0 */
* { margin: 0; padding: 0; font: inherit; color: inherit; }
*, :after, :before { box-sizing: border-box; }
:root {-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;text-size-adjust:100%;cursor:default;line-height:1.5;overflow-wrap:break-word;word-break:break-word;tab-size:4}
html, body { height:100%; color: #000; }
img, picture, video, canvas, svg { display: block;max-width:100%; }
button { background:none;border:0;cursor:pointer; }
a { text-decoration:none }
table { border-collapse:collapse;border-spacing:0 }
ul, li { list-style: none; }
+109
View File
@@ -0,0 +1,109 @@
@import url('./reset.css');
@import url('./font.css');
* { font-family: 'Pretendard Variable', 'Pretendard'; font-size: 14px; }
:root {--background-color: rgba(0, 0, 0, 0.3); --color1: #6944F0; --color2: #6944F0; --gray: rgba(255, 255, 255, 0.7); }
html,body { position: relative; font-size: 62.5%; }
body { overflow: hidden; user-select: none; }
.wrap { display: flex; flex-direction: column; height: 100%; }
/* -------------------- Main : Center -------------------- */
#canvas { width: 100vw; height: 100vh; position: relative; overflow: hidden; }
.btnGroup { display: flex; gap: 10px; position: absolute; left: 460px; top: 20px; }
.btnGroup > button { width: fit-content; font-size: 12px;}
.btnGroup > input { width: fit-content; font-size: 12px;}
.left { display: flex; flex-direction: column; position: absolute; top: 14px; left: 14px; width: 420px; max-height: calc(100% - 28px); padding: 14px; border-radius: 14px; background: rgba(255, 255, 255, 0.5); }
.left .side-resize { position: absolute; top: 0; right: -6px; width: 6px; height: 100%; background: none; cursor: ew-resize; }
.left .left-header .file-info { display: flex; align-items: center;}
.left .left-header .file-info .schema { width: fit-content; margin-right: 6px; padding: 0 4px; border-radius: 4px; color: white; background: gray; font-size: 12px; }
.left .left-header .file-info .file-name { flex: 1; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.left .left-header h2 { font-family: 'S-CoreDream-3Light'; font-size: 18px; font-weight: bold; }
.left .left-conts { display: flex; display: none; flex-direction: column; position: relative; overflow: auto; scrollbar-gutter: stable; margin-right: -14px; }
.tree-container { width: fit-content; min-width: -webkit-fill-available; background: var(--gray); user-select: none; font-size: 14px; }
.tree { list-style: none; margin: 8px 14px; width: 100%;}
.tree *:before { width: 14px; height: 14px; display: inline-block; }
.tree ul { list-style: none; margin-left: 10px; padding-left: 11px; border-left: 1px dashed #999;}
.tree ul:first-child { margin-left: 0; padding-left: 0; border-left: 0; }
.tree li { list-style: none; width: 100%; white-space: nowrap; }
.tree li > .input-caret { display: none; }
.tree li > .label-caret { display: inline-block; width: 14px; height: 14px; margin: 0 4px; background: url('../img/arrow-plus.svg') no-repeat center center/cover; transition: all 0.2s ease; cursor: pointer; }
.tree li > .input-caret:checked+.label-caret { background: url('../img/arrow-minus.svg') no-repeat center center/cover; transform: rotate(-360deg); }
.tree li > div { display: inline-block; }
.tree li > div > .ifc-type { display: inline-block; padding-left: 6px; color: gray; font-size: 14px; cursor: pointer; }
.tree li > div > .ifc-name { display: inline-block; padding-left: 6px; font-size: 14px; }
.tree li > div.selected { background-color: rgba(0, 123, 255, 0.2); }
.tree li .label-caret > span { margin-left: 14px; color: #000; }
.tree .input-caret:not(:checked)~ul { display: none; }
.tree li > .input-visible { display: none; }
.tree li > .label-visible { display: inline-block; width: 14px; height: 14px; margin-left: 4px; background: url('../img/hide.svg') no-repeat center center/cover; transition: all 0.2s ease; cursor: pointer; }
.tree li > .input-visible:checked+.label-visible { background: url('../img/show.svg') no-repeat center center/cover; transform: rotate(-360deg); }
.right { display: flex; flex-direction: column; position: absolute; top: 14px; right: -680px; width: 420px; max-height: calc(100% - 28px); padding: 14px; border-radius: 14px; background: rgba(255, 255, 255, 0.5); }
.right .side-resize { position: absolute; top: 0; left: -6px; width: 6px; height: 100%; background: none; cursor: ew-resize; }
.show-right { right: 14px !important; }
.right .right-header h2 { font-family: 'S-CoreDream-3Light'; font-size: 18px; font-weight: bold; }
.right .right-conts { display: flex; flex-direction: column; position: relative; overflow-y: auto; scrollbar-gutter: stable; margin-right: -14px; }
.accordion { margin-top: 8px;}
.accordion .accordion-header { display: flex; height: 32px; border-left: 2px solid var(--color2); border-bottom: 1px solid rgba(0, 0, 0, 0.1); background:var(--gray); }
.accordion .accordion-header .btn-accordion { display: flex; align-items: center; position: relative; width: 100%; padding-left: 14px; font-size: 16px; font-weight: bold; text-align: left; }
.accordion .accordion-header .btn-accordion img { width: 16px; height: 16px; margin-right: 7px; }
.accordion .accordion-header .btn-accordion::after { content: ""; display: inline-block; position: absolute; right: 4px; width: 16px; height: 16px; background: url('../img/caret.svg') no-repeat center center/cover; transition: all 0.2s; }
.accordion .accordion-header .btn-accordion.collapse::after { transform: rotate(-180deg); }
.accordion .accordion-collapse { max-height: 0; overflow: hidden; transition: all 0.2s; background:var(--gray); }
.accordion .accordion-collapse[aria-hidden="false"] { max-height: 5000px; /* A large enough value to accommodate content */ }
.accordion .accordion-collapse .accordion-body { display: flex; flex-direction: column; gap: 2px; padding: 14px; border-left: 2px solid var(--color2); }
.accordion .accordion-collapse .accordion-body .sub-title { margin-top: 7px; }
.accordion .accordion-collapse .accordion-body .sub-title:first-child { margin-top: 0; }
.accordion .accordion-collapse .accordion-body dl { display: flex; }
.accordion .accordion-collapse .accordion-body dl dt { width: 40%; padding-left: 8px; color: gray; word-break: break-all; }
.accordion .accordion-collapse .accordion-body dl dd { flex:1; padding-left: 8px; word-break: break-all; }
/* -------------------- Scrollbar -------------------- */
.scrollbar::-webkit-scrollbar { width: 16px; }
.scrollbar::-webkit-scrollbar-track { background-color: transparent; }
.scrollbar::-webkit-scrollbar-thumb { background-color: gray; border-radius: 20px; border: 6px solid transparent; background-clip: content-box; min-height: 50px; }
.scrollbar::-webkit-scrollbar-thumb:hover { background-color: var(--color1); }
.scrollbar::-webkit-scrollbar-corner { background: transparent; }
/* -------------------- Progress -------------------- */
.progress-wrap { display: flex; justify-content: center; align-items: center; position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
.progress-wrap .progress-content { display: flex; flex-direction: column; align-items: center; font-family: 'Protest Riot'; font-size: 30px; }
.loader { display: inline-block; position: relative; width: 48px; height: 48px; }
.loader::after, .loader::before { content: ''; position: absolute; left: 0; top: 0; width: 48px; height: 48px; border: 4px solid #FFF; box-sizing: border-box; animation: rotation 2s ease-in-out infinite; }
.loader::after { border-color: var(--color1); animation-delay: 1s; }
@keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* -------------------- Modal -------------------- */
.modal { display: none; justify-content: center; align-items: center; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.5); z-index: 1000; }
.modal-content { position: relative; width: 90%; max-width: 500px; padding: 20px; border-radius: 8px; background: #fff; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); animation: fadeIn 0.3s ease-out; }
.modal-content > .modal__conts { font-size: 18px; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
.highlight { color:darkred; font-family: 'Protest Riot'; font-size: 18px; font-weight: bold; }
/* -------------------- Media query -------------------- */
@media (max-width: 768px) {
.left { display: none }
.right { display: none }
}
+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="4" fill="#D9D9D9"/>
<path d="M15.1429 11H4.85714C4.62981 11 4.4118 10.8946 4.25105 10.7071C4.09031 10.5196 4 10.2652 4 10C4 9.73478 4.09031 9.48043 4.25105 9.29289C4.4118 9.10536 4.62981 9 4.85714 9H15.1429C15.3702 9 15.5882 9.10536 15.7489 9.29289C15.9097 9.48043 16 9.73478 16 10C16 10.2652 15.9097 10.5196 15.7489 10.7071C15.5882 10.8946 15.3702 11 15.1429 11Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="4" fill="#D9D9D9"/>
<path d="M10 5V15M15 10H5" stroke="#292828" stroke-width="1.66667" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5276 15.6023C19.1951 15.8933 18.6897 15.8596 18.3987 15.5271L12.0008 8.2152L5.6028 15.5271C5.3119 15.8596 4.8065 15.8933 4.474 15.6023C4.1415 15.3114 4.1078 14.806 4.3987 14.4735L12.0008 5.7854L19.6028 14.4735C19.8938 14.806 19.8601 15.3114 19.5276 15.6023Z" fill="#000" fill-opacity="1.0"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.4 17L3 15.6L8.6 10L3 4.4L4.4 3L10 8.6L15.6 3L17 4.4L11.4 10L17 15.6L15.6 17L10 11.4L4.4 17Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66683 4.39134L2.7335 3.33301L16.6668 17.2663L15.6085 18.333L13.0418 15.7663C12.0835 16.083 11.0668 16.2497 10.0002 16.2497C5.8335 16.2497 2.27516 13.658 0.833496 9.99967C1.4085 8.53301 2.32516 7.24134 3.49183 6.21634L1.66683 4.39134ZM10.0002 7.49967C10.6632 7.49967 11.2991 7.76307 11.7679 8.23191C12.2368 8.70075 12.5002 9.33663 12.5002 9.99967C12.5006 10.2835 12.4527 10.5653 12.3585 10.833L9.16683 7.64134C9.43455 7.54716 9.71636 7.49925 10.0002 7.49967ZM10.0002 3.74967C14.1668 3.74967 17.7252 6.34134 19.1668 9.99967C18.4863 11.7271 17.3306 13.2266 15.8335 14.3247L14.6502 13.133C15.8026 12.3359 16.7321 11.2573 17.3502 9.99967C16.6765 8.62456 15.6306 7.46603 14.3313 6.6558C13.032 5.84557 11.5314 5.41614 10.0002 5.41634C9.09183 5.41634 8.20016 5.56634 7.36683 5.83301L6.0835 4.55801C7.2835 4.04134 8.6085 3.74967 10.0002 3.74967ZM2.65016 9.99967C3.32378 11.3748 4.36971 12.5333 5.66903 13.3435C6.96834 14.1538 8.46892 14.5832 10.0002 14.583C10.5752 14.583 11.1418 14.5247 11.6668 14.408L9.76683 12.4997C9.18697 12.4375 8.64585 12.1787 8.23348 11.7664C7.8211 11.354 7.56232 10.8129 7.50016 10.233L4.66683 7.39134C3.84183 8.09967 3.15016 8.98301 2.65016 9.99967Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0002 7.5C9.33712 7.5 8.70124 7.76339 8.2324 8.23223C7.76355 8.70107 7.50016 9.33696 7.50016 10C7.50016 10.663 7.76355 11.2989 8.2324 11.7678C8.70124 12.2366 9.33712 12.5 10.0002 12.5C10.6632 12.5 11.2991 12.2366 11.7679 11.7678C12.2368 11.2989 12.5002 10.663 12.5002 10C12.5002 9.33696 12.2368 8.70107 11.7679 8.23223C11.2991 7.76339 10.6632 7.5 10.0002 7.5ZM10.0002 14.1667C8.89509 14.1667 7.83529 13.7277 7.05388 12.9463C6.27248 12.1649 5.8335 11.1051 5.8335 10C5.8335 8.89493 6.27248 7.83512 7.05388 7.05372C7.83529 6.27232 8.89509 5.83333 10.0002 5.83333C11.1052 5.83333 12.165 6.27232 12.9464 7.05372C13.7278 7.83512 14.1668 8.89493 14.1668 10C14.1668 11.1051 13.7278 12.1649 12.9464 12.9463C12.165 13.7277 11.1052 14.1667 10.0002 14.1667ZM10.0002 3.75C5.8335 3.75 2.27516 6.34167 0.833496 10C2.27516 13.6583 5.8335 16.25 10.0002 16.25C14.1668 16.25 17.7252 13.6583 19.1668 10C17.7252 6.34167 14.1668 3.75 10.0002 3.75Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+63
View File
@@ -0,0 +1,63 @@
<!doctype html><html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Document</title><link rel="stylesheet" href="./css/style.css"></head><body><canvas class="canvas" id="canvas"></canvas><div class="progress-wrap"><div class="progress-content" id="progress-text"></div></div><div class="modal"><div class="modal-content"><div class="modal__conts">• 모델 파일은 <span class="highlight">300MB</span>를 초과할 경우 지원되지 않습니다<br>• 파일 크기가 클 땐 웹 최적화된 GLB포맷 또는 IFC형식으로 변환해 보세요<br></div></div></div><div class="btnGroup"></div><div class="left" id="tree"><div class="side-resize"></div><div class="left-header"><div class="file-info"><div id="schema" class="schema">Version</div><div id="file-name" class="file-name"></div></div><h2>FileName</h2></div><div class="left-conts scrollbar"><div class="tree-container" id="tree-container"><ul class="tree"><li><input type="checkbox" id="root" class="input-caret" checked="checked"> <label for="root" class="label-caret"></label> <input type="checkbox" id="root-visible" class="input-visible"> <label for="root-visible" class="label-visible"></label><div><span class="ifc-type">node-02-01</span> <span class="ifc-name">Description1111111112222222444442222222221</span></div><ul><li><input type="checkbox" id="node-01" class="input-caret"> <label for="node-01" class="label-caret"></label> <input type="checkbox" id="node-01-visible" class="input-visible"> <label for="node-01-visible" class="label-visible"></label><div><span class="ifc-type">node-01</span> <span class="ifc-name">Description1111</span></div></li><li><input type="checkbox" id="node-02" class="input-caret"> <label for="node-02" class="label-caret"></label> <input type="checkbox" id="node-02-visible" class="input-visible"> <label for="node-02-visible" class="label-visible"></label><div><span class="ifc-type">node-02</span> <span class="ifc-name">Description2</span></div><ul><li><input type="checkbox" id="node-02-01" class="input-caret"> <label for="node-02-01" class="label-caret"></label> <input type="checkbox" id="node-02-01-visible" class="input-visible"> <label for="node-02-01-visible" class="label-visible"></label><div><span class="ifc-type">node-02-01</span> <span class="ifc-name">Description2</span></div></li><li><input type="checkbox" id="node-02-02" class="input-caret"> <label for="node-02-02" class="label-caret"></label> <input type="checkbox" id="node-02-02-visible" class="input-visible"> <label for="node-02-02-visible" class="label-visible"></label><div><span class="ifc-type">node-02-02</span> <span class="ifc-name">Description2</span></div></li></ul></li><li><input type="checkbox" id="node-03"> <label for="node-03" class="label-caret"></label> <input type="checkbox" id="node-03-visible" class="input-visible"> <label for="node-03-visible" class="label-visible"></label><div>node-03<span>Description</span></div></li></ul></li></ul></div></div></div><div class="right" id="property-table"><div class="side-resize"></div><div class="right-header"><h2>PROPERTIES</h2></div><div class="right-conts scrollbar"><div class="accordion"><div class="accordion-header"><button type="button" class="btn-accordion" aria-expanded="false" aria-controls="Attributes">Attributes</button></div><div class="accordion-collapse collapse" id="Attributes" aria-hidden="true"><div class="accordion-body"></div></div></div><div class="accordion"><div class="accordion-header"><button type="button" class="btn-accordion" aria-expanded="false" aria-controls="PropertySets">Property Sets</button></div><div class="accordion-collapse collapse" id="PropertySets" aria-hidden="true"><div class="accordion-body"></div></div></div><div class="accordion"><div class="accordion-header"><button type="button" class="btn-accordion" aria-expanded="false" aria-controls="Material">Material</button></div><div class="accordion-collapse collapse" id="Material" aria-hidden="true"><div class="accordion-body"></div></div></div><div class="accordion"><div class="accordion-header"><button type="button" class="btn-accordion" aria-expanded="false" aria-controls="SpatialContainer">Spatial Container</button></div><div class="accordion-collapse collapse" id="SpatialContainer" aria-hidden="true"><div class="accordion-body"></div></div></div></div></div><script>/* -------------------- Left, Right 사이드 패널 폭 조절 -------------------- */
const $leftResize = document.querySelector('.left .side-resize');
const $rightResize = document.querySelector('.right .side-resize');
const $left = document.querySelector('.left');
const $right = document.querySelector('.right');
let activeResize = null; // "left" 또는 "right" 값을 가짐
let startX = 0;
let initialWidth = 0;
// 왼쪽 리사이저: 왼쪽 요소의 너비 조절
$leftResize.addEventListener('mousedown', (event) => {
activeResize = 'left';
startX = event.clientX;
initialWidth = $left.getBoundingClientRect().width;
});
// 오른쪽 리사이저: 오른쪽 요소의 너비 조절
$rightResize.addEventListener('mousedown', (event) => {
activeResize = 'right';
startX = event.clientX;
initialWidth = $right.getBoundingClientRect().width;
});
// 마우스 업 시, 드래그 종료
document.addEventListener('mouseup', () => {
activeResize = null;
});
// 마우스 이동 시, 해당하는 요소의 너비 조절
document.addEventListener('mousemove', (event) => {
if (!activeResize) return;
if (activeResize === 'left') {
// 왼쪽 요소의 경우, 오른쪽으로 드래그하면 너비 증가
const newWidth = initialWidth + (event.clientX - startX);
handleResize($left, newWidth);
} else if (activeResize === 'right') {
// 오른쪽 요소의 경우, 보통 리사이저가 왼쪽 경계에 있으므로
// 마우스를 왼쪽으로 드래그하면 너비 증가
const newWidth = initialWidth - (event.clientX - startX);
handleResize($right, newWidth);
}
});
// 사이즈 조절 동작 함수 (최소, 최대값 적용)
function handleResize(elem, newWidth) {
if (!elem) return;
if (newWidth <= 300) {
elem.style.width = '300px';
} else if (newWidth >= 650) {
elem.style.width = '650px';
} else {
elem.style.width = `${newWidth}px`;
}
}</script><script src="https://api.digitalarchive.work/hmCesium/lib/axios/dist/axios.js"></script><script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script><script type="importmap">{
"imports": {
"three": "../node_modules/three/build/three.module.js",
"three/addons/": "../node_modules/three/examples/jsm/",
"@tweenjs/tween.js": "../node_modules/@tweenjs/tween.js/dist/tween.esm.js"
}
}</script><script defer="defer" src="./bundle.669a5fff259e4110fa12.js"></script><script defer="defer" src="./bundle.26e9bcb072f8f12dccbf.js"></script></body></html>
+46
View File
@@ -0,0 +1,46 @@
# Basis Universal GPU Texture Compression
Basis Universal is a "[supercompressed](http://gamma.cs.unc.edu/GST/gst.pdf)"
GPU texture and texture video compression system that outputs a highly
compressed intermediate file format (.basis) that can be quickly transcoded to
a wide variety of GPU texture compression formats.
[GitHub](https://github.com/BinomialLLC/basis_universal)
## Transcoders
Basis Universal texture data may be used in two different file formats:
`.basis` and `.ktx2`, where `ktx2` is a standardized wrapper around basis texture data.
For further documentation about the Basis compressor and transcoder, refer to
the [Basis GitHub repository](https://github.com/BinomialLLC/basis_universal).
The folder contains two files required for transcoding `.basis` or `.ktx2` textures:
* `basis_transcoder.js` — JavaScript wrapper for the WebAssembly transcoder.
* `basis_transcoder.wasm` — WebAssembly transcoder.
Both are dependencies of `KTX2Loader`:
```js
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath( 'examples/jsm/libs/basis/' );
ktx2Loader.detectSupport( renderer );
ktx2Loader.load( 'diffuse.ktx2', function ( texture ) {
const material = new THREE.MeshStandardMaterial( { map: texture } );
}, function () {
console.log( 'onProgress' );
}, function ( e ) {
console.error( e );
} );
```
## License
[Apache License 2.0](https://github.com/BinomialLLC/basis_universal/blob/master/LICENSE)
File diff suppressed because one or more lines are too long
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
# Draco 3D Data Compression
Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.
[Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco)
## Contents
This folder contains three utilities:
* `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser.
* `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices.
* `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder.
Each file is provided in two variations:
* **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco).
* **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension).
Either variation may be used with `DRACOLoader`:
```js
var dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('path/to/decoders/');
dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
```
Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder).
## License
[Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE)
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
# Basis Universal GPU Texture Compression
Basis Universal is a "[supercompressed](http://gamma.cs.unc.edu/GST/gst.pdf)"
GPU texture and texture video compression system that outputs a highly
compressed intermediate file format (.basis) that can be quickly transcoded to
a wide variety of GPU texture compression formats.
[GitHub](https://github.com/BinomialLLC/basis_universal)
## Transcoders
Basis Universal texture data may be used in two different file formats:
`.basis` and `.ktx2`, where `ktx2` is a standardized wrapper around basis texture data.
For further documentation about the Basis compressor and transcoder, refer to
the [Basis GitHub repository](https://github.com/BinomialLLC/basis_universal).
The folder contains two files required for transcoding `.basis` or `.ktx2` textures:
* `basis_transcoder.js` — JavaScript wrapper for the WebAssembly transcoder.
* `basis_transcoder.wasm` — WebAssembly transcoder.
Both are dependencies of `KTX2Loader`:
```js
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath( 'examples/jsm/libs/basis/' );
ktx2Loader.detectSupport( renderer );
ktx2Loader.load( 'diffuse.ktx2', function ( texture ) {
const material = new THREE.MeshStandardMaterial( { map: texture } );
}, function () {
console.log( 'onProgress' );
}, function ( e ) {
console.error( e );
} );
```
## License
[Apache License 2.0](https://github.com/BinomialLLC/basis_universal/blob/master/LICENSE)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,32 @@
# Draco 3D Data Compression
Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.
[Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco)
## Contents
This folder contains three utilities:
* `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser.
* `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices.
* `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder.
Each file is provided in two variations:
* **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco).
* **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension).
Either variation may be used with `DRACOLoader`:
```js
var dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('path/to/decoders/');
dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
```
Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder).
## License
[Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE)
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
/* 폰트 */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR&family=Protest+Riot&display=swap');
@font-face {
font-family: 'S-CoreDream-3Light';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_six@1.2/S-CoreDream-3Light.woff') format('woff');
font-style: normal;
}
@font-face {
font-family: 'SeoulNamsanM';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_two@1.0/SeoulNamsanM.woff') format('woff');
font-weight: normal;
font-style: normal;
}
/* @font-face {
font-family: "Nova Flat", system-ui;
font-weight: 400;
font-style: normal;
src:url('https://fonts.googleapis.com/css2?family=Nova+Flat&display=swap');
} */
/* 폰트적용 */
/* * { font-family: 'Noto Sans KR', 'Protest Riot', 'Gowun Dodum', sans-serif; } */
+712
View File
@@ -0,0 +1,712 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GSIM Viewer</title>
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="./reset.css">
<link rel="stylesheet" href="./system.css">
<!-- <link rel="icon" href=""> -->
</head>
<body style="overflow: hidden; user-select: none;">
<div id="mapContainer"></div>
<!-- 헤더부분 ===== ===== ===== ===== ===== -->
<header>
<div class="header-left">
<img class="icon" src="./svg/gsim-logo.svg" alt="gsim-logo">
<h3 id="project-title">건설공사</h3>
</div>
</header>
<!-- 320px 이하 오류 메시지 ===== ===== ===== ===== ===== -->
<div class="notice">
<img class="icon" src="./svg/error.svg" alt="error">
<h5>이 해상도는 지원하지 않습니다.</h5>
</div>
<!-- [[[[[ [[[[[ [[[[[ [[[[[ [[[[[ 작성하는 곳 ]]]]] ]]]]] ]]]]] ]]]]] ]]]]] -->
<!-- 분할비교 slider -->
<div class="split-slider" id="slider" style="display: none;">
<div class="split-icon" id="slider-icon">
<img class="icon" src="./svg/icon-split.svg" alt="icon-split">
</div>
</div>
<main>
<!-- 상단부분 -->
<div class="top">
<!-- 상단 왼쪽 부분 -->
<div class="top-left">
<!-- 모델기반 left -->
<div class="window-big" id="model-list">
<div class="window-header">
<h3 id="left-list-title">모델기반(3D)</h3>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
<div class="window-body">
<ul></ul>
</div>
</div>
</div>
<!-- 상단 오른쪽 부분 -->
<div class="top-right">
<!-- 모델기반 right -->
<div class="window-big" id="model-list-right" style="display: none;">
<div class="window-header">
<h3 id="right-list-title">우측화면 모델</h3>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
<div class="window-body">
<ul></ul>
</div>
</div>
<!-- 라벨 가져오기 -->
<div class="window-big" id="label-get">
<div class="window-header">
<h3>라벨 가져오기</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="dropdown">
<button class="dropdown-toggle">
<h4 id="label-get-selected">라벨을 가져올 모델을 선택하세요.</h4>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</button>
<div class="dropdown-menu">
<ul id="label-get-list">
<li>[공사중] 2공구 중간태</li>
<li>[공사중] 1공구 중간태</li>
<li>[공사중] 4공구 설계 시설</li>
</ul>
</div>
</div>
<div class="window-btn-wrap">
<!-- <div class="big-btn" id="">
<h3>삭제</h3>
</div> -->
<div class="big-btn" id="label-get-submit">
<h3>적용</h3>
</div>
</div>
</div>
</div>
<!-- 라벨추가 -->
<div class="window-big" id="func-label-add" style="display: none;">
<div class="window-header">
<h3>라벨 속성</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="window-content">
<h4>라벨 텍스트</h4>
<input type="text" id="modal-label-text" name="label-text" placeholder="라벨 내용 입력" />
</div>
<div class="window-content">
<h4>라벨크기</h4>
<div class="font-size" id="modal-label-size">
<label>
<input type="radio" name="fontsize" value="10">
<span class="btn">10pt</span>
</label>
<label>
<input type="radio" name="fontsize" value="15">
<span class="btn">12pt</span>
</label>
<label>
<input type="radio" name="fontsize" value="20">
<span class="btn">14pt</span>
</label>
<label>
<input type="radio" name="fontsize" value="25" checked>
<span class="btn">16pt</span>
</label>
</div>
</div>
<div class="window-content">
<h4>배경색</h4>
<div class="color-picker" id="modal-label-color">
<label>
<input type="radio" name="color" value="#F21D0D" />
<span class="color-swatch" style="background-color: var(--color-red);"></span>
</label>
<label>
<input type="radio" name="color" value="#B92ED1" />
<span class="color-swatch" style="background-color: var(--color-magenta);"></span>
</label>
<label>
<input type="radio" name="color" value="#6D3DC2" />
<span class="color-swatch" style="background-color: var(--color-purple);"></span>
</label>
<label>
<input type="radio" name="color" value="#03AEFC" />
<span class="color-swatch" style="background-color: var(--color-cyan);"></span>
</label>
<label>
<input type="radio" name="color" value="#4DB251" />
<span class="color-swatch" style="background-color: var(--color-green);"></span>
</label>
<label>
<input type="radio" name="color" value="#FFBF00" />
<span class="color-swatch" style="background-color: var(--color-yellow);"></span>
</label>
<label>
<input type="radio" name="color" value="#A0705F" />
<span class="color-swatch" style="background-color: var(--color-brown);"></span>
</label>
<label>
<input type="radio" name="color" value="#7F7F7F" />
<span class="color-swatch" style="background-color: var(--color-iron);"></span>
</label>
<label>
<input type="radio" name="color" value="#688897" />
<span class="color-swatch" style="background-color: var(--color-steel);"></span>
</label>
<label>
<input type="radio" name="color" id="last-swatch" value="#000000" checked/>
<span class="color-swatch"
style="background-color: #000000;"></span>
</label>
</div>
</div>
<div class="window-content">
<h4>배경 투명도</h4>
<div class="z-scaleBar-gauge">
<input type="range" min="0" max="1" step="0.25" value="0" class="slider" id="modal-label-alpha">
</div>
<img class="label-opacity-number" src="./svg/label-opacity-number.svg"
alt="label-opacity-number">
</div>
<div class="window-btn-wrap">
<div class="big-btn" id="modal-label-delete">
<h3>삭제</h3>
</div>
<div class="big-btn" id="modal-label-submit">
<h3>적용</h3>
</div>
</div>
</div>
</div>
<!-- 라벨 -->
<div class="window-big" id="func-label" style="display: none;">
<div class="window-header">
<h3>라벨</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close" style="display: none;">
</div>
<div class="window-body" style="display: none;">
<h4>라벨을 적용할 모델</h4>
<div class="file-click">
<h4 class="file-title">[공사 중] 설계 변경</h4>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
</div>
<div class="window-footer">
<ul>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>인주JCT 1교, L=25m</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>인주JCT 1교, L=25m</h4>
</li>
</ul>
</div>
</div>
<!-- 이슈추가 -->
<div class="window-big" id="func-issue-add" style="display: none;">
<div class="window-header">
<h3>이슈 속성</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="window-content">
<h4>작성자와 날짜</h4>
<div class="file-click">
<h4 id="modal-issue-writer">이동호 선임연구원</h4>
<h4 id="modal-issue-date">2025-00-00</h4>
</div>
</div>
<div class="window-content">
<h4>제목</h4>
<input type="text" id="modal-issue-text" name="label-text" placeholder="제목 입력" />
<!-- <img class="icon" src="./svg/icon-calendar.svg" alt="icon-calendar"> -->
</div>
<div class="window-content">
<h4>상세내용</h4>
<textarea class="issue-txt" id="modal-issue-content" name="issue-txt" id="/"
placeholder="상세 내용 입력"></textarea>
</div>
<div class="window-content" style="display: none;"><!-- 첨부파일 임시 제거 -->
<div class="window-content-header">
<h4>첨부파일</h4>
<div class="xs-icon-btn" id="">
<img class="icon" src="./svg/icon-add.svg" alt="icon-add">
<p style="color: var(--grayscale-lv0-background) !important;">파일추가</p>
</div>
</div>
<div class="file-click">
<ul>
<li>
<h4>사용자 시방서.pdf</h4>
<img class="icon" src="./svg/icon-delete.svg" alt="icon-delete">
</li>
</ul>
</div>
</div>
<div class="window-btn-wrap">
<div class="big-btn" id="modal-issue-cancel">
<h3>삭제</h3>
</div>
<div class="big-btn" id="modal-issue-submit">
<h3>적용</h3>
</div>
</div>
</div>
</div>
<!-- 이슈 -->
<div class="window-big" id="func-issue" style="display: none;">
<div class="window-header">
<h3>이슈</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close" style="display: none;">
</div>
<div class="window-body" style="display: none;">
<h4>이슈를 적용할 모델</h4>
<div class="file-click">
<h4 class="file-title">[공사 중] 설계 변경</h4>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
</div>
<div class="window-footer">
<ul>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>이슈1</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>이슈 추가2</h4>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 창뜨는 부분 -->
<div class="bottom-up">
<div class="bottom-up-left">
<!-- 좌표변환 창 -->
<div class="window" id="select-coordi" style="display: none;">
<div class="window-header">
<h3>좌표변환</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close" id="select-coordi-close">
</div>
<div class="window-body">
<div class="window-body-content">
<p>위치표시</p>
<label class="radio-label">
<input type="radio" name="gcs" id="gcs1" checked><span
class="radio-custom-inbox"></span>위도,
경도
</label>
<label class="radio-label">
<input type="radio" name="gcs" id="gcs2"><span class="radio-custom-inbox"></span>토목좌표
(x,y)
</label>
</div>
<div class="window-body-content">
<p>투영원점</p>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5185" disabled><span class="radio-custom-inbox"></span>서부
</label>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5186" disabled><span class="radio-custom-inbox"></span>중부
</label>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5187" disabled><span class="radio-custom-inbox"></span>동부
</label>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5188" disabled><span class="radio-custom-inbox"></span>동해
</label>
</div>
</div>
</div>
</div>
<!-- 선형클리핑 상단 뜨는 곳 -->
<div class="window-relative" style="display: none;" id="clipping-key-map">
<div class="window-footer">
<ul id="key-map-list">
<li>
<img class="icon" src="./svg/icon-label-dot-red.svg" alt="icon-label-dot-red">
<h4>인주JCT 1교, L=25m</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-red.svg" alt="icon-label-dot-red">
<h4>인주JCT 1교, L=25m</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-red.svg" alt="icon-label-dot-red">
<h4>인주JCT 1교, L=25m</h4>
</li>
</ul>
</div>
<div class="window-img-container">
<img class="icon" src="./svg/icon-close-aaa.svg" alt="icon-close-aaa">
<svg id="key-map" width="512" height="512"></svg>
</div>
</div>
<!-- 아래 상단 가운데 -->
<div class="bottom-up-center">
<!-- 선형클리핑 -->
<div class="window-fit" id="func-clipping" style="display: none;">
<div class="xs-icon-btn" id="clipping-camera-follow">
<img class="icon" src="./svg/icon-vision.svg" alt="icon-vision">
<p>카메라 따라가기</p>
</div>
<div class="xs-icon-btn" id="clipping-inverse">
<img class="icon" src="./svg/icon-rotate.svg" alt="icon-rotate">
<p>카메라 반전</p>
</div>
<div class="xs-icon-btn" id="clipping-camera-play">
<img class="icon" src="./svg/icon-play.svg" alt="icon-play">
</div>
<div class="xs-icon-btn">
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff" id="compare-road2">
<p id="compare-road">지방도628호선1</p>
<div class="z-scaleBar-gauge" id="compare-toolbar">
<input type="range" min="0" max="100" step="1" value="0" class="slider">
</div>
<p class="fixed-width" id="station-number">4+300</p>
</div>
</div>
<!-- 투명도 -->
<div class="window-fit" id="func-opacity" style="display: none;">
<div class="xs-icon-btn">
<div class="z-scaleBar-gauge">
<input type="range" min="0" max="100" step="1" value="0" class="slider">
</div>
<p class="fixed-width">0%</p>
</div>
</div>
<!-- 측정 -->
<div class="window-fit" id="func-measurement" style="display: none;">
<div class="xs-icon-btn" id="slope-btn">
<img class="icon" src="./svg/icon-slope.svg" alt="icon-slope">
<p>경사도</p>
</div>
<div class="xs-icon-btn" id="location-btn">
<img class="icon" src="./svg/icon-locate.svg" alt="icon-locate">
<p>좌표</p>
</div>
<div class="xs-icon-btn" id="distance-btn">
<img class="icon" src="./svg/icon-beeline.svg" alt="icon-beeline">
<p>직선거리</p>
</div>
<div class="xs-icon-btn" id="horizontal-btn">
<img class="icon" src="./svg/icon-horizon.svg" alt="icon-horizon">
<p>수평거리</p>
</div>
<div class="xs-icon-btn" id="vertical-btn">
<img class="icon" src="./svg/icon-vertical.svg" alt="icon-vertical">
<p>수직거리</p>
</div>
<div class="xs-icon-btn" id="measure-delete-btn">
<p class="type-em-red">측정 전체삭제</p>
</div>
</div>
<!-- 라벨생성 -->
<div class="window-fit" id="func-label-bar" style="display: none;">
<div class="xs-icon-btn" id="label-add-btn">
<img class="icon" src="./svg/icon-add.svg" alt="icon-add">
<p>라벨 추가</p>
</div>
<div class="xs-icon-btn" id="label-get-btn">
<img class="icon" src="./svg/icon-download.svg" alt="icon-download">
<p>라벨 가져오기</p>
</div>
<div class="xs-icon-btn" id="label-delete-btn">
<p class="type-em-red">라벨 전체삭제</p>
</div>
</div>
<!-- 이슈생성 -->
<div class="window-fit" id="func-issue-bar" style="display: none;">
<div class="xs-icon-btn" id="issue-add-btn">
<img class="icon" src="./svg/icon-add.svg" alt="icon-add">
<p>이슈 추가</p>
</div>
<div class="xs-icon-btn" id="issue-delete-btn">
<p class="type-em-red">이슈 전체삭제</p>
</div>
</div>
</div>
<!-- 아래 상단 오른쪽부분 -->
<div class="bottom-up-right">
<!-- 레이어창 -->
<div class="window" id="layer-modal" style="display: none;">
<div class="window-header">
<h3>레이어</h3>
<img class="icon modal-close" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<label class="checkbox-label layer" id="wpb-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-linear.svg" alt="icon-linear" />
선형중심선
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="plane-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-cad.svg" alt="icon-cad" />
계획평면
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="siteLine-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-dottedLine.svg" alt="icon-dottedLine" />
용지라인
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="label-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-label.svg" alt="icon-label" />
라벨
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="issue-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-issue.svg" alt="icon-issue" />
이슈
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" style="display: none;">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-photo.svg" alt="icon-photo" />
촬영이미지
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
</div>
</div>
<!-- 기본지도창 -->
<div class="window" id="baseMap-modal" style="display: none;">
<div class="window-header">
<h3>기본지도</h3>
<img class="icon modal-close" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="window-body-content">
<p>국토교통부</p>
<label class="radio-label">
<input type="radio" name="map" id="molit-nomal" value="vworld-normal"><span
class="radio-custom-inbox"></span>일반
</label>
<label class="radio-label">
<input type="radio" name="map" id="molit-hybrid" value="vworld-hybrid"><span
class="radio-custom-inbox"></span>하이브리드
</label>
<label class="radio-label">
<input type="radio" name="map" id="molit-satellite" value="vworld-satellite"><span
class="radio-custom-inbox"></span>위성
</label>
</div>
<div class="window-body-content">
<p>Carto</p>
<label class="radio-label">
<input type="radio" name="map" id="carto-nomal" value="carto-normal" checked><span
class="radio-custom-inbox"></span>일반
</label>
<label class="radio-label">
<input type="radio" name="map" id="carto-light" value="carto-light"><span
class="radio-custom-inbox"></span>일반(라이트)
</label>
<label class="radio-label">
<input type="radio" name="map" id="carto-dark" value="carto-dark"><span
class="radio-custom-inbox"></span>일반(다크)
</label>
</div>
<div class="window-body-content">
<p>Google</p>
<label class="radio-label">
<input type="radio" name="map" id="google-nomal" value="google-normal"><span
class="radio-custom-inbox"></span>일반
</label>
<label class="radio-label">
<input type="radio" name="map" id="google-hybrid" value="google-hybrid"><span
class="radio-custom-inbox"></span>하이브리드
</label>
<label class="radio-label">
<input type="radio" name="map" id="google-satellite" value="google-satellite"><span
class="radio-custom-inbox"></span>위성
</label>
</div>
</div>
</div>
</div>
</div>
<!-- 하단 툴킷 모음 부분 ===== ===== ===== ===== ===== -->
<div class="bottom">
<!-- z-스케일창 -->
<div class="z-scaleBar">
<div class="z-scaleBar-gauge">
<input type="range" min="0" max="10" step="1" value="1" class="slider" id="zScale-slider"
style="background: linear-gradient(to right, #fff 10%, #aaa 10%);">
</div>
<img src="./svg/z-scale-number.svg" alt="z-scale-number">
</div>
<!-- 가운데 하단 툴킷 창 -->
<div class="center-tool-kit" id="func-btns" style="display: none;">
<div class="xs-icon-btn" id="func-split-btn">
<img class="icon" src="./svg/icon-division.svg" alt="icon-division">
<p>분할비교</p>
</div>
<div class="xs-icon-btn" id="func-clipping-btn">
<img class="icon" src="./svg/icon-cliping.svg" alt="icon-cliping">
<p>선형 클리핑</p>
</div>
<div class="xs-icon-btn" id="func-opacity-btn" style="display: none;">
<img class="icon" src="./svg/icon-opacity.svg" alt="icon-opacity">
<p>투명도</p>
</div>
<div class="xs-icon-btn" id="func-measurement-btn">
<img class="icon" src="./svg/icon-measurement.svg" alt="icon-measurement">
<p>측정</p>
</div>
<div class="xs-icon-btn" id="func-label-btn" style="display: none;">
<img class="icon" src="./svg/icon-newLabel.svg" alt="icon-newLabel">
<p>라벨 생성</p>
</div>
<div class="xs-icon-btn" id="func-issue-btn" style="display: none;">
<img class="icon" src="./svg/icon-newIssue.svg" alt="icon-newIssue">
<p>이슈 생성</p>
</div>
<div class="xs-icon-btn" style="display: none;">
<img class="icon" src="./svg/icon-newPhoto.svg" alt="icon-newPhoto">
<p>큐피트 수정</p>
</div>
</div>
<!-- 오른쪽 하단 툴킷 창 -->
<div class="right-tool-kit">
<div class="xs-icon-btn" id="set-location">
<img class="icon" src="./svg/icon-originLocate.svg" alt="icon-originLocate">
<p>원래위치</p>
</div>
<div class="xs-icon-btn" id="set-north">
<img class="icon" src="./svg/icon-northface.svg" alt="icon-northface">
<p>정북표시</p>
</div>
<div class="xs-icon-btn" id="set-topView">
<img class="icon" src="./svg/icon-topView.svg" alt="icon-topView">
<p>탑뷰</p>
</div>
<div class="xs-icon-btn" id="set-layer" style="display: none;">
<img class="icon" src="./svg/icon-layer.svg" alt="icon-layer">
<p>레이어</p>
</div>
<div class="xs-icon-btn" id="set-baseMap">
<img class="icon" src="./svg/icon-map.svg" alt="icon-map">
<p>기본지도</p>
</div>
</div>
</div>
</main>
<!-- [[[[[ [[[[[ [[[[[ [[[[[ [[[[[ 작성하는 끝 ]]]]] ]]]]] ]]]]] ]]]]] ]]]]] -->
<!-- 푸터부분 ===== ===== ===== ===== ===== -->
<footer>
<div class="footer-left">
<!-- <a href="/">
<p>사용법</p>
</a>
<img class="icon" src="/svg/dot-777.svg" alt="dot-777">
<p>오류 문의 : 홍길동A 수석연구원</p> -->
<div class="footer-middle">
<button class="coordinate">
<p>좌표변환</p>
</button>
</div>
</div>
<div class="footer-right">
<img class="icon" src="./svg/hanmaceng-logo.svg" alt="hanmaceng-logo">
<p>Copyright Ⓒ Hanmaceng Corp. All Rights Reserved.</p>
</div>
</footer>
<div id="progress">
<img src="./loading.gif">
<div>loading...<br>잠시만 기다려주세요.</div>
</div>
<div id="changeCursor"></div>
</body>
</html>
<!-- <script src="../../lib/Cesium/Build/Cesium/Cesium.js"></script>
<link href="../../lib/Cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<script src="../../lib/axios/dist/axios.js"></script>
<script src="../../lib/proj4/dist/proj4.js"></script> -->
<script src="http://gsim.hanmaceng.co.kr:5151/data/lib/lib/Cesium/Build/Cesium/Cesium.js"></script>
<link href="http://gsim.hanmaceng.co.kr:5151/data/lib/lib/Cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<script src="http://gsim.hanmaceng.co.kr:5151/data/lib/lib/axios/dist/axios.js"></script>
<script src="http://gsim.hanmaceng.co.kr:5151/data/lib/lib/proj4/dist/proj4.js"></script>
<script src="./main.js" type="module"></script>
<script src="./style.js"></script>
+713
View File
@@ -0,0 +1,713 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GSIM Viewer</title>
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="./reset.css">
<link rel="stylesheet" href="./system.css">
<!-- <link rel="icon" href=""> -->
</head>
<body style="overflow: hidden; user-select: none;">
<div id="mapContainer"></div>
<!-- 헤더부분 ===== ===== ===== ===== ===== -->
<header>
<div class="header-left">
<img class="header-icon" src="./svg/gsim-logo.svg" alt="gsim-logo">
<h3 id="project-title">건설공사</h3>
</div>
</header>
<!-- 320px 이하 오류 메시지 ===== ===== ===== ===== ===== -->
<div class="notice">
<img class="icon" src="./svg/error.svg" alt="error">
<h5>이 해상도는 지원하지 않습니다.</h5>
</div>
<!-- [[[[[ [[[[[ [[[[[ [[[[[ [[[[[ 작성하는 곳 ]]]]] ]]]]] ]]]]] ]]]]] ]]]]] -->
<!-- 분할비교 slider -->
<div class="split-slider" id="slider" style="display: none;">
<div class="split-icon" id="slider-icon">
<img class="split-icon-img" src="./svg/icon-split.svg" alt="icon-split">
</div>
</div>
<main>
<!-- 상단부분 -->
<div class="top">
<!-- 상단 왼쪽 부분 -->
<div class="top-left">
<!-- 모델기반 left -->
<div class="window-big" id="model-list">
<div class="window-header">
<h3 id="left-list-title">모델기반(3D)</h3>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
<div class="window-body not">
<ul></ul>
</div>
</div>
</div>
<!-- 상단 오른쪽 부분 -->
<div class="top-right">
<!-- 모델기반 right -->
<div class="window-big" id="model-list-right" style="display: none;">
<div class="window-header">
<h3 id="right-list-title">우측화면 모델</h3>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
<div class="window-body">
<ul></ul>
</div>
</div>
<!-- 라벨 가져오기 -->
<div class="window-big" id="label-get" style="display: none;">
<div class="window-header">
<h3>라벨 가져오기</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="dropdown">
<button class="dropdown-toggle">
<h4 id="label-get-selected">라벨을 가져올 모델을 선택하세요.</h4>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</button>
<div class="dropdown-menu">
<ul id="label-get-list">
<li>[공사중] 2공구 중간태</li>
<li>[공사중] 1공구 중간태</li>
<li>[공사중] 4공구 설계 시설</li>
</ul>
</div>
</div>
<div class="window-btn-wrap">
<!-- <div class="big-btn" id="">
<h3>삭제</h3>
</div> -->
<div class="big-btn" id="label-get-submit">
<h3>적용</h3>
</div>
</div>
</div>
</div>
<!-- 라벨추가 -->
<div class="window-big" id="func-label-add" style="display: none;">
<div class="window-header">
<h3>라벨 속성</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="window-content">
<h4>라벨 텍스트</h4>
<input type="text" id="modal-label-text" name="label-text" placeholder="라벨 내용 입력" />
</div>
<div class="window-content">
<h4>라벨크기</h4>
<div class="font-size" id="modal-label-size">
<label>
<input type="radio" name="fontsize" value="10">
<span class="btn">10pt</span>
</label>
<label>
<input type="radio" name="fontsize" value="15">
<span class="btn">12pt</span>
</label>
<label>
<input type="radio" name="fontsize" value="20">
<span class="btn">14pt</span>
</label>
<label>
<input type="radio" name="fontsize" value="25" checked>
<span class="btn">16pt</span>
</label>
</div>
</div>
<div class="window-content">
<h4>배경색</h4>
<div class="color-picker" id="modal-label-color">
<label>
<input type="radio" name="color" value="#F21D0D" />
<span class="color-swatch" style="background-color: var(--color-red);"></span>
</label>
<label>
<input type="radio" name="color" value="#B92ED1" />
<span class="color-swatch" style="background-color: var(--color-magenta);"></span>
</label>
<label>
<input type="radio" name="color" value="#6D3DC2" />
<span class="color-swatch" style="background-color: var(--color-purple);"></span>
</label>
<label>
<input type="radio" name="color" value="#03AEFC" />
<span class="color-swatch" style="background-color: var(--color-cyan);"></span>
</label>
<label>
<input type="radio" name="color" value="#4DB251" />
<span class="color-swatch" style="background-color: var(--color-green);"></span>
</label>
<label>
<input type="radio" name="color" value="#FFBF00" />
<span class="color-swatch" style="background-color: var(--color-yellow);"></span>
</label>
<label>
<input type="radio" name="color" value="#A0705F" />
<span class="color-swatch" style="background-color: var(--color-brown);"></span>
</label>
<label>
<input type="radio" name="color" value="#7F7F7F" />
<span class="color-swatch" style="background-color: var(--color-iron);"></span>
</label>
<label>
<input type="radio" name="color" value="#688897" />
<span class="color-swatch" style="background-color: var(--color-steel);"></span>
</label>
<label>
<input type="radio" name="color" id="last-swatch" value="#000000" checked/>
<span class="color-swatch"
style="background-color: #000000;"></span>
</label>
</div>
</div>
<div class="window-content">
<h4>배경 투명도</h4>
<div class="z-scaleBar-gauge">
<input type="range" min="0" max="1" step="0.25" value="0" class="slider" id="modal-label-alpha">
</div>
<img class="label-opacity-number" src="./svg/label-opacity-number.svg"
alt="label-opacity-number">
</div>
<div class="window-btn-wrap">
<div class="big-btn" id="modal-label-delete">
<h3>삭제</h3>
</div>
<div class="big-btn" id="modal-label-submit">
<h3>적용</h3>
</div>
</div>
</div>
</div>
<!-- 라벨 -->
<div class="window-big" id="func-label" style="display: none;">
<div class="window-header">
<h3>라벨</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close" style="display: none;">
</div>
<div class="window-body" style="display: none;">
<h4>라벨을 적용할 모델</h4>
<div class="file-click">
<h4 class="file-title">[공사 중] 설계 변경</h4>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
</div>
<div class="window-footer">
<ul>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>인주JCT 1교, L=25m</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>인주JCT 1교, L=25m</h4>
</li>
</ul>
</div>
</div>
<!-- 이슈추가 -->
<div class="window-big" id="func-issue-add" style="display: none;">
<div class="window-header">
<h3>이슈 속성</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="window-content">
<h4>작성자와 날짜</h4>
<div class="file-click">
<h4 id="modal-issue-writer">이동호 선임연구원</h4>
<h4 id="modal-issue-date">2025-00-00</h4>
</div>
</div>
<div class="window-content">
<h4>제목</h4>
<input type="text" id="modal-issue-text" name="label-text" placeholder="제목 입력" />
<!-- <img class="icon" src="./svg/icon-calendar.svg" alt="icon-calendar"> -->
</div>
<div class="window-content">
<h4>상세내용</h4>
<textarea class="issue-txt" id="modal-issue-content" name="issue-txt" id="/"
placeholder="상세 내용 입력"></textarea>
</div>
<div class="window-content" style="display: none;"><!-- 첨부파일 임시 제거 -->
<div class="window-content-header">
<h4>첨부파일</h4>
<div class="xs-icon-btn" id="">
<img class="icon" src="./svg/icon-add.svg" alt="icon-add">
<p style="color: var(--grayscale-lv0-background) !important;">파일추가</p>
</div>
</div>
<div class="file-click subfit">
<ul>
<li>
<h4>사용자 시방서.pdf</h4>
<img class="icon" src="./svg/icon-delete.svg" alt="icon-delete">
</li>
</ul>
</div>
</div>
<div class="window-btn-wrap">
<div class="big-btn" id="modal-issue-cancel">
<h3>삭제</h3>
</div>
<div class="big-btn" id="modal-issue-submit">
<h3>적용</h3>
</div>
</div>
</div>
</div>
<!-- 이슈 -->
<div class="window-big" id="func-issue" style="display: none;">
<div class="window-header">
<h3>이슈</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close" style="display: none;">
</div>
<div class="window-body" style="display: none;">
<h4>이슈를 적용할 모델</h4>
<div class="file-click">
<h4 class="file-title">[공사 중] 설계 변경</h4>
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff">
</div>
</div>
<div class="window-footer">
<ul>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>이슈1</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-white.svg" alt="icon-label-dot-white">
<h4>이슈 추가2</h4>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 창뜨는 부분 -->
<div class="bottom-up">
<div class="bottom-up-left">
<!-- 좌표변환 창 -->
<div class="window" id="select-coordi" style="display: none;">
<div class="window-header">
<h3>좌표변환</h3>
<img class="icon" src="./svg/icon-close.svg" alt="icon-close" id="select-coordi-close">
</div>
<div class="window-body">
<div class="window-body-content">
<p>위치표시</p>
<label class="radio-label">
<input type="radio" name="gcs" id="gcs1" checked><span
class="radio-custom-inbox"></span>위도,
경도
</label>
<label class="radio-label">
<input type="radio" name="gcs" id="gcs2"><span class="radio-custom-inbox"></span>토목좌표
(x,y)
</label>
</div>
<div class="window-body-content">
<p>투영원점</p>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5185" disabled><span class="radio-custom-inbox"></span>서부
</label>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5186" disabled><span class="radio-custom-inbox"></span>중부
</label>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5187" disabled><span class="radio-custom-inbox"></span>동부
</label>
<label class="radio-label" style="opacity: 25%; cursor: not-allowed;">
<input type="radio" name="pcs" id="5188" disabled><span class="radio-custom-inbox"></span>동해
</label>
</div>
</div>
</div>
</div>
<!-- 선형클리핑 상단 뜨는 곳 -->
<div class="window-relative" style="display: none;" id="clipping-key-map">
<div class="window-footer">
<ul id="key-map-list">
<li>
<img class="icon" src="./svg/icon-label-dot-red.svg" alt="icon-label-dot-red">
<h4>인주JCT 1교, L=25m</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-red.svg" alt="icon-label-dot-red">
<h4>인주JCT 1교, L=25m</h4>
</li>
<li>
<img class="icon" src="./svg/icon-label-dot-red.svg" alt="icon-label-dot-red">
<h4>인주JCT 1교, L=25m</h4>
</li>
</ul>
</div>
<div class="window-img-container">
<img class="icon" src="./svg/icon-close-aaa.svg" alt="icon-close-aaa">
<svg id="key-map" width="512" height="512"></svg>
</div>
</div>
<!-- 아래 상단 가운데 -->
<div class="bottom-up-center">
<!-- 선형클리핑 -->
<div class="window-fit min-set" id="func-clipping" style="display: none;">
<div class="xs-icon-btn" id="clipping-camera-follow">
<img class="icon" src="./svg/icon-vision.svg" alt="icon-vision">
<p>카메라 따라가기</p>
</div>
<div class="xs-icon-btn" id="clipping-inverse">
<img class="icon" src="./svg/icon-rotate.svg" alt="icon-rotate">
<p>카메라 반전</p>
</div>
<div class="xs-icon-btn" id="clipping-camera-play">
<img class="icon" src="./svg/icon-play.svg" alt="icon-play">
</div>
<div class="xs-icon-btn">
<img class="icon" src="./svg/icon-sign-down-fff.svg" alt="icon-sign-down-fff" id="compare-road2">
<p id="compare-road">지방도628호선1</p>
<div class="z-scaleBar-gauge" id="compare-toolbar">
<input type="range" min="0" max="100" step="1" value="0" class="slider">
</div>
<p class="fixed-width" id="station-number">4+300</p>
</div>
</div>
<!-- 투명도 -->
<div class="window-fit" id="func-opacity" style="display: none;">
<div class="xs-icon-btn">
<div class="z-scaleBar-gauge">
<input type="range" min="0" max="100" step="1" value="0" class="slider">
</div>
<p class="fixed-width">0%</p>
</div>
</div>
<!-- 측정 -->
<div class="window-fit" id="func-measurement" style="display: none;">
<div class="xs-icon-btn" id="slope-btn">
<img class="icon" src="./svg/icon-slope.svg" alt="icon-slope">
<p>경사도</p>
</div>
<div class="xs-icon-btn" id="location-btn">
<img class="icon" src="./svg/icon-locate.svg" alt="icon-locate">
<p>좌표</p>
</div>
<div class="xs-icon-btn" id="distance-btn">
<img class="icon" src="./svg/icon-beeline.svg" alt="icon-beeline">
<p>직선거리</p>
</div>
<div class="xs-icon-btn" id="horizontal-btn">
<img class="icon" src="./svg/icon-horizon.svg" alt="icon-horizon">
<p>수평거리</p>
</div>
<div class="xs-icon-btn" id="vertical-btn">
<img class="icon" src="./svg/icon-vertical.svg" alt="icon-vertical">
<p>수직거리</p>
</div>
<div class="xs-icon-btn" id="measure-delete-btn">
<p class="type-em-red">측정 전체삭제</p>
</div>
</div>
<!-- 라벨생성 -->
<div class="window-fit" id="func-label-bar" style="display: none;">
<div class="xs-icon-btn" id="label-add-btn">
<img class="icon" src="./svg/icon-add.svg" alt="icon-add">
<p>라벨 추가</p>
</div>
<div class="xs-icon-btn" id="label-get-btn">
<img class="icon" src="./svg/icon-download.svg" alt="icon-download">
<p>라벨 가져오기</p>
</div>
<div class="xs-icon-btn" id="label-delete-btn">
<p class="type-em-red">라벨 전체삭제</p>
</div>
</div>
<!-- 이슈생성 -->
<div class="window-fit" id="func-issue-bar" style="display: none;">
<div class="xs-icon-btn" id="issue-add-btn">
<img class="icon" src="./svg/icon-add.svg" alt="icon-add">
<p>이슈 추가</p>
</div>
<div class="xs-icon-btn" id="issue-delete-btn">
<p class="type-em-red">이슈 전체삭제</p>
</div>
</div>
</div>
<!-- 아래 상단 오른쪽부분 -->
<div class="bottom-up-right">
<!-- 레이어창 -->
<div class="window" id="layer-modal">
<div class="window-header">
<h3>레이어</h3>
<img class="icon modal-close" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<label class="checkbox-label layer" id="wpb-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-linear.svg" alt="icon-linear" />
선형중심선
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="plane-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-cad.svg" alt="icon-cad" />
계획평면
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="siteLine-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-dottedLine.svg" alt="icon-dottedLine" />
용지라인
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="label-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-label.svg" alt="icon-label" />
라벨
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" id="issue-layer-btn">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-issue.svg" alt="icon-issue" />
이슈
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
<label class="checkbox-label layer" style="display: none;">
<div class="checkbox-label-left">
<input type="checkbox" name="layer" id="linear"
data-icon-visible="./svg/icon-visibility.svg"
data-icon-invisible="./svg/icon-invisibility.svg" />
<span class="checkbox-custom-inbox"></span>
<img class="icon" src="./svg/icon-photo.svg" alt="icon-photo" />
촬영이미지
</div>
<img class="icon visibility-icon" src="./svg/icon-invisibility.svg" alt="icon-visibility" />
</label>
</div>
</div>
<!-- 기본지도창 -->
<div class="window" id="baseMap-modal" style="display: none;">
<div class="window-header">
<h3>기본지도</h3>
<img class="icon modal-close" src="./svg/icon-close.svg" alt="icon-close">
</div>
<div class="window-body">
<div class="window-body-content">
<p>국토교통부</p>
<label class="radio-label">
<input type="radio" name="map" id="molit-nomal" value="vworld-normal"><span
class="radio-custom-inbox"></span>일반
</label>
<label class="radio-label">
<input type="radio" name="map" id="molit-hybrid" value="vworld-hybrid"><span
class="radio-custom-inbox"></span>하이브리드
</label>
<label class="radio-label">
<input type="radio" name="map" id="molit-satellite" value="vworld-satellite"><span
class="radio-custom-inbox"></span>위성
</label>
</div>
<div class="window-body-content">
<p>Carto</p>
<label class="radio-label">
<input type="radio" name="map" id="carto-nomal" value="carto-normal" checked><span
class="radio-custom-inbox"></span>일반
</label>
<label class="radio-label">
<input type="radio" name="map" id="carto-light" value="carto-light"><span
class="radio-custom-inbox"></span>일반(라이트)
</label>
<label class="radio-label">
<input type="radio" name="map" id="carto-dark" value="carto-dark"><span
class="radio-custom-inbox"></span>일반(다크)
</label>
</div>
<div class="window-body-content">
<p>Google</p>
<label class="radio-label">
<input type="radio" name="map" id="google-nomal" value="google-normal"><span
class="radio-custom-inbox"></span>일반
</label>
<label class="radio-label">
<input type="radio" name="map" id="google-hybrid" value="google-hybrid"><span
class="radio-custom-inbox"></span>하이브리드
</label>
<label class="radio-label">
<input type="radio" name="map" id="google-satellite" value="google-satellite"><span
class="radio-custom-inbox"></span>위성
</label>
</div>
</div>
</div>
</div>
</div>
<!-- 하단 툴킷 모음 부분 ===== ===== ===== ===== ===== -->
<div class="bottom">
<!-- z-스케일창 -->
<div class="z-scaleBar">
<div class="z-scaleBar-gauge">
<input type="range" min="0" max="10" step="1" value="1" class="slider" id="zScale-slider"
style="background: linear-gradient(to right, #fff 10%, #777 10%);">
</div>
<img class="z-scale-number-rem" src="./svg/z-scale-number.svg" alt="z-scale-number">
</div>
<!-- 가운데 하단 툴킷 창 -->
<div class="center-tool-kit" id="func-btns" style="display: none;">
<div class="xs-icon-btn" id="func-split-btn">
<img class="icon" src="./svg/icon-division.svg" alt="icon-division">
<p>분할비교</p>
</div>
<div class="xs-icon-btn" id="func-clipping-btn">
<img class="icon" src="./svg/icon-cliping.svg" alt="icon-cliping">
<p>선형 클리핑</p>
</div>
<div class="xs-icon-btn" id="func-opacity-btn" style="display: none;">
<img class="icon" src="./svg/icon-opacity.svg" alt="icon-opacity">
<p>투명도</p>
</div>
<div class="xs-icon-btn" id="func-measurement-btn">
<img class="icon" src="./svg/icon-measurement.svg" alt="icon-measurement">
<p>측정</p>
</div>
<div class="xs-icon-btn" id="func-label-btn" style="display: none;">
<img class="icon" src="./svg/icon-newLabel.svg" alt="icon-newLabel">
<p>라벨 생성</p>
</div>
<div class="xs-icon-btn" id="func-issue-btn" style="display: none;">
<img class="icon" src="./svg/icon-newIssue.svg" alt="icon-newIssue">
<p>이슈 생성</p>
</div>
<div class="xs-icon-btn" style="display: none;">
<img class="icon" src="./svg/icon-newPhoto.svg" alt="icon-newPhoto">
<p>큐피트 수정</p>
</div>
</div>
<!-- 오른쪽 하단 툴킷 창 -->
<div class="right-tool-kit">
<div class="xs-icon-btn" id="set-location">
<img class="icon" src="./svg/icon-originLocate.svg" alt="icon-originLocate">
<p>원래위치</p>
</div>
<div class="xs-icon-btn" id="set-north">
<img class="icon" src="./svg/icon-northface.svg" alt="icon-northface">
<p>정북표시</p>
</div>
<div class="xs-icon-btn" id="set-topView">
<img class="icon" src="./svg/icon-topView.svg" alt="icon-topView">
<p>탑뷰</p>
</div>
<div class="xs-icon-btn on" id="set-layer" style="display: none;">
<img class="icon" src="./svg/icon-layer.svg" alt="icon-layer">
<p>레이어</p>
</div>
<div class="xs-icon-btn" id="set-baseMap">
<img class="icon" src="./svg/icon-map.svg" alt="icon-map">
<p>기본지도</p>
</div>
</div>
</div>
</main>
<!-- [[[[[ [[[[[ [[[[[ [[[[[ [[[[[ 작성하는 끝 ]]]]] ]]]]] ]]]]] ]]]]] ]]]]] -->
<!-- 푸터부분 ===== ===== ===== ===== ===== -->
<footer>
<div class="footer-left">
<!-- <a href="/">
<p>사용법</p>
</a>
<img class="icon" src="/svg/dot-777.svg" alt="dot-777">
<p>오류 문의 : 홍길동A 수석연구원</p> -->
<div class="footer-middle">
<button class="coordinate">
<p>좌표변환</p>
</button>
</div>
</div>
<div class="footer-right">
<img class="footer-icon" src="./svg/hanmaceng-logo.svg" alt="hanmaceng-logo">
<p>Copyright Ⓒ HANMAC FAMILY All Rights Reserved.</p>
</div>
</footer>
<div id="progress">
<img src="./loading.gif">
<div>loading...<br>잠시만 기다려주세요.</div>
</div>
<div id="changeCursor"></div>
</body>
</html>
<!-- <script src="../../lib/Cesium/Build/Cesium/Cesium.js"></script>
<link href="../../lib/Cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<script src="../../lib/axios/dist/axios.js"></script>
<script src="../../lib/proj4/dist/proj4.js"></script> -->
<script src="https://api.digitalarchive.work/hmCesium/lib/Cesium/Build/Cesium/Cesium.js"></script>
<link href="https://api.digitalarchive.work/hmCesium/lib/Cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<script src="https://api.digitalarchive.work/hmCesium/lib/axios/dist/axios.js"></script>
<script src="https://api.digitalarchive.work/hmCesium/lib/proj4/dist/proj4.js"></script>
<script src="https://api.digitalarchive.work/hmCesium/hmCesium.min.js" type="module"></script>
<script src="./main.js" type="module"></script>
<script src="./style.js"></script>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="white"/>
<path d="M7.93734 9.104L5.979 7.14567C5.86789 7.36789 5.78123 7.59706 5.719 7.83317C5.65623 8.06928 5.62484 8.30539 5.62484 8.5415C5.62484 9.33317 5.90623 10.0104 6.469 10.5732C7.03123 11.1354 7.70817 11.4165 8.49984 11.4165C8.69428 11.4165 8.86789 11.4026 9.02067 11.3748C9.17345 11.3471 9.32623 11.3054 9.479 11.2498L12.0415 13.7915C12.2082 13.9582 12.4026 14.0415 12.6248 14.0415C12.8471 14.0415 13.0415 13.9582 13.2082 13.7915L13.7915 13.2082C13.9582 13.0415 14.0415 12.8471 14.0415 12.6248C14.0415 12.4026 13.9582 12.2082 13.7915 12.0415L11.2498 9.479C11.3054 9.32623 11.3471 9.184 11.3748 9.05234C11.4026 8.92011 11.4165 8.73595 11.4165 8.49984C11.4165 7.70817 11.1354 7.03095 10.5732 6.46817C10.0104 5.90595 9.33317 5.62484 8.5415 5.62484C8.30539 5.62484 8.06928 5.65595 7.83317 5.71817C7.59706 5.78095 7.36789 5.86789 7.14567 5.979L9.104 7.93734L7.93734 9.104ZM9.99984 18.3332C8.84706 18.3332 7.76373 18.1143 6.74984 17.6765C5.73595 17.2393 4.854 16.6457 4.104 15.8957C3.354 15.1457 2.76039 14.2637 2.32317 13.2498C1.88539 12.2359 1.6665 11.1526 1.6665 9.99984C1.6665 8.84706 1.88539 7.76373 2.32317 6.74984C2.76039 5.73595 3.354 4.854 4.104 4.104C4.854 3.354 5.73595 2.76011 6.74984 2.32234C7.76373 1.88511 8.84706 1.6665 9.99984 1.6665C11.1526 1.6665 12.2359 1.88511 13.2498 2.32234C14.2637 2.76011 15.1457 3.354 15.8957 4.104C16.6457 4.854 17.2393 5.73595 17.6765 6.74984C18.1143 7.76373 18.3332 8.84706 18.3332 9.99984C18.3332 11.1526 18.1143 12.2359 17.6765 13.2498C17.2393 14.2637 16.6457 15.1457 15.8957 15.8957C15.1457 16.6457 14.2637 17.2393 13.2498 17.6765C12.2359 18.1143 11.1526 18.3332 9.99984 18.3332Z" fill="#0D8DF2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="15" height="15" rx="7.5" fill="#C6006B" stroke="white"/>
<path d="M4.27114 4.58789C4.23349 4.62052 4.20324 4.66082 4.18241 4.70609C4.16159 4.75136 4.15067 4.80056 4.15039 4.85039V12.2004C4.15039 12.2932 4.18727 12.3822 4.2529 12.4479C4.31854 12.5135 4.40756 12.5504 4.50039 12.5504C4.59322 12.5504 4.68224 12.5135 4.74788 12.4479C4.81352 12.3822 4.85039 12.2932 4.85039 12.2004V10.2653C6.02245 9.33957 7.0322 9.83876 8.19508 10.4145C8.91258 10.7693 9.6852 11.1517 10.5138 11.1517C11.1233 11.1517 11.7625 10.9439 12.431 10.3642C12.4686 10.3316 12.4989 10.2913 12.5197 10.246C12.5405 10.2007 12.5514 10.1515 12.5517 10.1017V4.85039C12.5515 4.78321 12.5321 4.71749 12.4956 4.66109C12.4591 4.60468 12.4071 4.55997 12.3459 4.5323C12.2847 4.50462 12.2168 4.49514 12.1503 4.505C12.0839 4.51486 12.0217 4.54363 11.9711 4.58789C10.7461 5.64795 9.70839 5.13432 8.5057 4.53889C7.2597 3.92114 5.84702 3.22245 4.27114 4.58789ZM11.8504 9.93632C10.6783 10.8621 9.66858 10.3624 8.5057 9.78714C7.41195 9.24682 6.19527 8.64395 4.85039 9.41964V5.0162C6.02245 4.09045 7.0322 4.58964 8.19508 5.16495C9.28883 5.70526 10.506 6.30813 11.8504 5.53245V9.93632Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="15" height="15" rx="7.5" fill="#E05C60" stroke="white"/>
<path d="M4.27114 4.58789C4.23349 4.62052 4.20324 4.66082 4.18241 4.70609C4.16159 4.75136 4.15067 4.80056 4.15039 4.85039V12.2004C4.15039 12.2932 4.18727 12.3822 4.2529 12.4479C4.31854 12.5135 4.40756 12.5504 4.50039 12.5504C4.59322 12.5504 4.68224 12.5135 4.74788 12.4479C4.81352 12.3822 4.85039 12.2932 4.85039 12.2004V10.2653C6.02245 9.33957 7.0322 9.83876 8.19508 10.4145C8.91258 10.7693 9.6852 11.1517 10.5138 11.1517C11.1233 11.1517 11.7625 10.9439 12.431 10.3642C12.4686 10.3316 12.4989 10.2913 12.5197 10.246C12.5405 10.2007 12.5514 10.1515 12.5517 10.1017V4.85039C12.5515 4.78321 12.5321 4.71749 12.4956 4.66109C12.4591 4.60468 12.4071 4.55997 12.3459 4.5323C12.2847 4.50462 12.2168 4.49514 12.1503 4.505C12.0839 4.51486 12.0217 4.54363 11.9711 4.58789C10.7461 5.64795 9.70839 5.13432 8.5057 4.53889C7.2597 3.92114 5.84702 3.22245 4.27114 4.58789ZM11.8504 9.93632C10.6783 10.8621 9.66858 10.3624 8.5057 9.78714C7.41195 9.24682 6.19527 8.64395 4.85039 9.41964V5.0162C6.02245 4.09045 7.0322 4.58964 8.19508 5.16495C9.28883 5.70526 10.506 6.30813 11.8504 5.53245V9.93632Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="15" height="15" rx="7.5" fill="#18A0FB" stroke="white"/>
<path d="M4.27114 4.58789C4.23349 4.62052 4.20324 4.66082 4.18241 4.70609C4.16159 4.75136 4.15067 4.80056 4.15039 4.85039V12.2004C4.15039 12.2932 4.18727 12.3822 4.2529 12.4479C4.31854 12.5135 4.40756 12.5504 4.50039 12.5504C4.59322 12.5504 4.68224 12.5135 4.74788 12.4479C4.81352 12.3822 4.85039 12.2932 4.85039 12.2004V10.2653C6.02245 9.33957 7.0322 9.83876 8.19508 10.4145C8.91258 10.7693 9.6852 11.1517 10.5138 11.1517C11.1233 11.1517 11.7625 10.9439 12.431 10.3642C12.4686 10.3316 12.4989 10.2913 12.5197 10.246C12.5405 10.2007 12.5514 10.1515 12.5517 10.1017V4.85039C12.5515 4.78321 12.5321 4.71749 12.4956 4.66109C12.4591 4.60468 12.4071 4.55997 12.3459 4.5323C12.2847 4.50462 12.2168 4.49514 12.1503 4.505C12.0839 4.51486 12.0217 4.54363 11.9711 4.58789C10.7461 5.64795 9.70839 5.13432 8.5057 4.53889C7.2597 3.92114 5.84702 3.22245 4.27114 4.58789ZM11.8504 9.93632C10.6783 10.8621 9.66858 10.3624 8.5057 9.78714C7.41195 9.24682 6.19527 8.64395 4.85039 9.41964V5.0162C6.02245 4.09045 7.0322 4.58964 8.19508 5.16495C9.28883 5.70526 10.506 6.30813 11.8504 5.53245V9.93632Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="15" height="15" rx="7.5" fill="#54803F" stroke="white"/>
<path d="M4.27114 4.58789C4.23349 4.62052 4.20324 4.66082 4.18241 4.70609C4.16159 4.75136 4.15067 4.80056 4.15039 4.85039V12.2004C4.15039 12.2932 4.18727 12.3822 4.2529 12.4479C4.31854 12.5135 4.40756 12.5504 4.50039 12.5504C4.59322 12.5504 4.68224 12.5135 4.74788 12.4479C4.81352 12.3822 4.85039 12.2932 4.85039 12.2004V10.2653C6.02245 9.33957 7.0322 9.83876 8.19508 10.4145C8.91258 10.7693 9.6852 11.1517 10.5138 11.1517C11.1233 11.1517 11.7625 10.9439 12.431 10.3642C12.4686 10.3316 12.4989 10.2913 12.5197 10.246C12.5405 10.2007 12.5514 10.1515 12.5517 10.1017V4.85039C12.5515 4.78321 12.5321 4.71749 12.4956 4.66109C12.4591 4.60468 12.4071 4.55997 12.3459 4.5323C12.2847 4.50462 12.2168 4.49514 12.1503 4.505C12.0839 4.51486 12.0217 4.54363 11.9711 4.58789C10.7461 5.64795 9.70839 5.13432 8.5057 4.53889C7.2597 3.92114 5.84702 3.22245 4.27114 4.58789ZM11.8504 9.93632C10.6783 10.8621 9.66858 10.3624 8.5057 9.78714C7.41195 9.24682 6.19527 8.64395 4.85039 9.41964V5.0162C6.02245 4.09045 7.0322 4.58964 8.19508 5.16495C9.28883 5.70526 10.506 6.30813 11.8504 5.53245V9.93632Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+21
View File
@@ -0,0 +1,21 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_f_3239_308)">
<circle cx="16" cy="16" r="14" fill="#0D8DF2" fill-opacity="0.6"/>
</g>
<g filter="url(#filter1_f_3239_308)">
<circle cx="16" cy="16" r="10" fill="#0D8DF2"/>
</g>
<circle cx="16" cy="16" r="3" fill="white"/>
<defs>
<filter id="filter0_f_3239_308" x="0" y="0" width="32" height="32" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_3239_308"/>
</filter>
<filter id="filter1_f_3239_308" x="4" y="4" width="24" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_3239_308"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.74952 3.75V16.25C8.74952 16.4158 8.68367 16.5747 8.56646 16.6919C8.44925 16.8092 8.29028 16.875 8.12452 16.875C7.95875 16.875 7.79978 16.8092 7.68257 16.6919C7.56536 16.5747 7.49952 16.4158 7.49952 16.25V10.625H3.38311L4.8167 12.0578C4.93398 12.1751 4.99986 12.3341 4.99986 12.5C4.99986 12.6659 4.93398 12.8249 4.8167 12.9422C4.69943 13.0595 4.54037 13.1253 4.37452 13.1253C4.20866 13.1253 4.0496 13.0595 3.93233 12.9422L1.43233 10.4422C1.37422 10.3841 1.32812 10.3152 1.29667 10.2393C1.26521 10.1635 1.24902 10.0821 1.24902 10C1.24902 9.91787 1.26521 9.83654 1.29667 9.76066C1.32812 9.68479 1.37422 9.61586 1.43233 9.55781L3.93233 7.05781C4.0496 6.94054 4.20866 6.87465 4.37452 6.87465C4.54037 6.87465 4.69943 6.94054 4.8167 7.05781C4.93398 7.17509 4.99986 7.33415 4.99986 7.5C4.99986 7.66585 4.93398 7.82491 4.8167 7.94219L3.38311 9.375H7.49952V3.75C7.49952 3.58424 7.56536 3.42527 7.68257 3.30806C7.79978 3.19085 7.95875 3.125 8.12452 3.125C8.29028 3.125 8.44925 3.19085 8.56646 3.30806C8.68367 3.42527 8.74952 3.58424 8.74952 3.75ZM18.5667 9.55781L16.0667 7.05781C15.9494 6.94054 15.7904 6.87465 15.6245 6.87465C15.4587 6.87465 15.2996 6.94054 15.1823 7.05781C15.0651 7.17509 14.9992 7.33415 14.9992 7.5C14.9992 7.66585 15.0651 7.82491 15.1823 7.94219L16.6159 9.375H12.4995V3.75C12.4995 3.58424 12.4337 3.42527 12.3165 3.30806C12.1992 3.19085 12.0403 3.125 11.8745 3.125C11.7088 3.125 11.5498 3.19085 11.4326 3.30806C11.3154 3.42527 11.2495 3.58424 11.2495 3.75V16.25C11.2495 16.4158 11.3154 16.5747 11.4326 16.6919C11.5498 16.8092 11.7088 16.875 11.8745 16.875C12.0403 16.875 12.1992 16.8092 12.3165 16.6919C12.4337 16.5747 12.4995 16.4158 12.4995 16.25V10.625H16.6159L15.1823 12.0578C15.0651 12.1751 14.9992 12.3341 14.9992 12.5C14.9992 12.6659 15.0651 12.8249 15.1823 12.9422C15.2996 13.0595 15.4587 13.1253 15.6245 13.1253C15.7904 13.1253 15.9494 13.0595 16.0667 12.9422L18.5667 10.4422C18.6248 10.3841 18.6709 10.3152 18.7024 10.2393C18.7338 10.1635 18.75 10.0821 18.75 10C18.75 9.91787 18.7338 9.83654 18.7024 9.76066C18.6709 9.68479 18.6248 9.61586 18.5667 9.55781Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="29" height="20" viewBox="0 0 29 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="29" height="20" rx="4" fill="#E7F4FE"/>
<path d="M8.47656 6.52344C8.47656 7.80078 9.26172 9.10156 11.0195 9.62891L10.2578 10.8008C9.0625 10.4258 8.21875 9.6582 7.73828 8.69141C7.25195 9.74023 6.38477 10.584 5.13672 11L4.36328 9.82812C6.13281 9.25391 6.95312 7.87109 6.95312 6.52344V6.37109H4.76172V5.17578H10.6211V6.37109H8.47656V6.52344ZM13.2461 4.42578V12.1367H11.7461V4.42578H13.2461ZM13.5273 13.6953V14.8906H6.09766V11.4336H7.60938V13.6953H13.5273ZM23.6116 4.41406V11.1055H22.1936V8.26953H21.3381V10.7539H19.9436V4.61328H21.3381V7.05078H22.1936V4.41406H23.6116ZM19.6272 5.51562V6.67578H14.3889V5.51562H16.2756V4.50781H17.7639V5.51562H19.6272ZM17.0256 7.02734C18.385 7.02734 19.3459 7.77734 19.3577 8.89062C19.3459 9.99219 18.385 10.7422 17.0256 10.7422C15.6897 10.7422 14.717 9.99219 14.717 8.89062C14.717 7.77734 15.6897 7.02734 17.0256 7.02734ZM17.0256 8.10547C16.4397 8.10547 16.053 8.39844 16.053 8.89062C16.053 9.38281 16.4397 9.6875 17.0256 9.67578C17.6116 9.6875 17.9983 9.38281 18.01 8.89062C17.9983 8.39844 17.6116 8.10547 17.0256 8.10547ZM19.967 11.1992C22.2756 11.1992 23.6584 11.9023 23.6702 13.1445C23.6584 14.375 22.2756 15.0781 19.967 15.0781C17.6702 15.0781 16.2756 14.375 16.2756 13.1445C16.2756 11.9023 17.6702 11.1992 19.967 11.1992ZM19.967 12.3242C18.5256 12.3242 17.7639 12.582 17.7756 13.1445C17.7639 13.6836 18.5256 13.9531 19.967 13.9531C21.4202 13.9531 22.1584 13.6836 22.1584 13.1445C22.1584 12.582 21.4202 12.3242 19.967 12.3242Z" fill="#0D8DF2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
@charset "utf-8";
/* Copyright Ⓒ Hanmaceng Corp. All Rights Reserved. */
/* 기술개발센터 김건우A 연구원 b25013@hanmaceng.co.kr */
/* 버전 히스토리 */
/* 2025-03-12 : -- reset.css 정의 */
/* 2025-03-19 : -- '여백 초기화'부분 수정 */
/* -- ↓ -- style -- ↓ -- */
/* 여백 초기화 ===== ===== ===== ===== ===== */
body,
div,
ul,
li,
dl,
dd,
dt,
ol,
h1,
h2,
h3,
h4,
h5,
h6,
input,
fieldset,
legend,
p,
select,
table,
th,
td,
tr,
textarea,
button,
form,
figure,
figcaption { margin: 0; padding: 0; line-height: 1; }
/* a 링크 초기화 ===== ===== ===== ===== ===== */
a { color: inherit; text-decoration: inherit; }
/* 폰트 스타일 초기화 (기울임) ===== ===== ===== ===== ===== */
em,
address { font-style: normal; }
/* 블릿기호 초기화 ===== ===== ===== ===== ===== */
ul,
li,
ol { list-style: none; }
/* 제목 태그 초기화 ===== ===== ===== ===== ===== */
h1,
h2,
h3,
h4,
h5,
h6 { font-size: inherit; font-weight: inherit; }
/* 버튼 초기화 ===== ===== ===== ===== ===== */
button, input { border: none; font: inherit; }
/* 테이블 테두리 초기화 ===== ===== ===== ===== ===== */
table { border-collapse: collapse; border-spacing: 0; }
/* 콜아웃 초기화 ===== ===== ===== ===== ===== */
blockquote,
q {quotes: none; }
/* 콜아웃 따옴표 초기화 ===== ===== ===== ===== ===== */
blockquote:before,
blockquote:after,
q:before,
q:after {content: '';content: none; }
/* box-sizing 초기화 ===== ===== ===== ===== ===== */
*,
*::before,
*::after { box-sizing: border-box; }
/* 요소 스타일 초기화 ===== ===== ===== ===== ===== */
article, aside, footer, header, nav, section { display: block; }
+116
View File
@@ -0,0 +1,116 @@
{
"type" : "total",
"title" : "고속국도 제30호 서산영덕선(인주염치) 건설공사",
"origin" : "172.16.41.49:4141",
"project" : "iyall2",
"projectCode" : "iyall2",
"terrain" : "",
"flyTo" : [126.94763032488123, 36.83988626283285, 19526.88102815474],
"query" : {
"setLabel" : "/gsim/setLabel",
"setIssue" : "/gsim/setIssue",
"issue_file" : "/gsim/issue_file",
"issue_delete":"/gsim/issue_delete",
"cupid":"/gsim/cupid"
},
"model" : {
"2019.08__[설계납품]설계 지반" : {
"tileset" : {
"terrain" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.08__[설계납품]설계 지반/bim/3dtile/terrain/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.08__[설계납품]설계 지반/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.08__[설계납품]설계 지반/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.08__[설계납품]설계 지반/bim/RoadDatas.json"
}
},
"2019.09__[설계납품]설계 시설" : {
"tileset" : {
"terrain" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.09__[설계납품]설계 시설/bim/3dtile/terrain/tileset.json",
"structure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.09__[설계납품]설계 시설/bim/3dtile/structure/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.09__[설계납품]설계 시설/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.09__[설계납품]설계 시설/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.09__[설계납품]설계 시설/bim/RoadDatas.json"
}
},
"2019.12__설계확인 시설rev01(거더길이)대체용" : {
"tileset" : {
"terrain" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev01(거더길이)대체용/bim/3dtile/terrain/tileset.json",
"substructure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev01(거더길이)대체용/bim/3dtile/substructure/tileset.json",
"girder" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev01(거더길이)대체용/bim/3dtile/girder/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev01(거더길이)대체용/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev01(거더길이)대체용/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev01(거더길이)대체용/bim/RoadDatas.json"
}
},
"2019.12__설계확인 시설rev02(P5, 편경사)" : {
"tileset" : {
"terrain" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/bim/3dtile/terrain/tileset.json",
"substructure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/bim/3dtile/substructure/tileset.json",
"girder" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/bim/3dtile/girder/tileset.json",
"deck" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/bim/3dtile/deck/tileset.json",
"structure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/bim/3dtile/structure/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2019.12__설계확인 시설rev02(P5, 편경사)/bim/RoadDatas.json"
}
},
"2023.05__[공사 중]설계 변경" : {
"tileset" : {
"terrain" : "http://172.16.41.49:5000/BCMF/iyall2/model/2023.05__[공사 중]설계 변경/bim/3dtile/terrain/tileset.json",
"structure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2023.05__[공사 중]설계 변경/bim/3dtile/structure/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2023.05__[공사 중]설계 변경/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2023.05__[공사 중]설계 변경/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2023.05__[공사 중]설계 변경/bim/RoadDatas.json"
}
},
"2024.07__[공사 중]1공구 중간태 1차" : {
"tileset" : {
"structure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]1공구 중간태 1차/bim/3dtile/structure/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]1공구 중간태 1차/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]1공구 중간태 1차/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]1공구 중간태 1차/bim/RoadDatas.json"
}
},
"2024.07__[공사 중]2공구 중간태 1차" : {
"tileset" : {
"structure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]2공구 중간태 1차/bim/3dtile/structure/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]2공구 중간태 1차/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]2공구 중간태 1차/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.07__[공사 중]2공구 중간태 1차/bim/RoadDatas.json"
}
},
"2024.10__[공사 중]1공구 중간태 2차" : {
"tileset" : {
"structure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]1공구 중간태 2차/bim/3dtile/structure/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]1공구 중간태 2차/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]1공구 중간태 2차/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]1공구 중간태 2차/bim/RoadDatas.json"
}
},
"2024.10__[공사 중]2공구 중간태 2차" : {
"tileset" : {
"structure" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]2공구 중간태 2차/bim/3dtile/structure/tileset.json"
},
"dLayer" : {
"label" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]2공구 중간태 2차/label/label.json",
"issue" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]2공구 중간태 2차/issue/issue.json",
"wpb" : "http://172.16.41.49:5000/BCMF/iyall2/model/2024.10__[공사 중]2공구 중간태 2차/bim/RoadDatas.json"
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
document.querySelectorAll('.slider').forEach(slider => {
slider.addEventListener('input', function () {
let min = parseFloat(this.min);
let max = parseFloat(this.max);
let curVal = parseFloat(this.value);
let val = ((curVal-min)/(max-min))*100;
// if (slider.id == 'zScale-slider') {
// this.style.background = `linear-gradient(to right, #fff ${val * 10}%, #aaa ${val * 10}%)`;
// } else {
this.style.background = `linear-gradient(to right, #fff ${val}%, #aaa ${val}%)`;
// }
});
});
document.querySelectorAll('input[type="checkbox"][name="layer"]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
if(checkbox.closest('.checkbox-label').classList.contains('disabled')){
checkbox.checked = false;
}
const label = checkbox.closest('.checkbox-label');
const visibilityIcon = label.querySelector('.visibility-icon');
const visibleSrc = checkbox.dataset.iconVisible;
const invisibleSrc = checkbox.dataset.iconInvisible;
if (!checkbox.checked) {
// label.style.opacity = '0.2';
if (visibilityIcon) visibilityIcon.src = invisibleSrc;
} else {
// label.style.opacity = '1';
if (visibilityIcon) visibilityIcon.src = visibleSrc;
}
});
});
document.querySelectorAll('input[type="checkbox"][name="list-set"]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
const label = checkbox.closest('.checkbox-label');
const visibilityIcon = label.querySelector('.visibility-icon');
const visibleSrc = checkbox.dataset.iconVisible;
const invisibleSrc = checkbox.dataset.iconInvisible;
if (!checkbox.checked) {
// label.style.opacity = '0.2';
if (visibilityIcon) visibilityIcon.src = invisibleSrc;
} else {
// label.style.opacity = '1';
if (visibilityIcon) visibilityIcon.src = visibleSrc;
}
});
});
document.querySelector('.dropdown-toggle').addEventListener('click', function () {
document.querySelector('.dropdown').classList.toggle('open');
});
+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.07312 11.8203C7.41687 11.8203 6.87781 11.2812 6.87781 10.6133C6.87781 9.95703 7.41687 9.41797 8.07312 9.41797C8.74109 9.41797 9.28016 9.95703 9.28016 10.6133C9.28016 11.2812 8.74109 11.8203 8.07312 11.8203Z" fill="#777777"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.14437 12.5938C6.9725 12.5938 6.035 11.6562 6.035 10.4844C6.035 9.32812 6.9725 8.39062 8.14437 8.39062C9.30062 8.39062 10.2381 9.32812 10.2381 10.4844C10.2381 11.6562 9.30062 12.5938 8.14437 12.5938Z" fill="#8FA8A4"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.07312 11.8203C7.41687 11.8203 6.87781 11.2812 6.87781 10.6133C6.87781 9.95703 7.41687 9.41797 8.07312 9.41797C8.74109 9.41797 9.28016 9.95703 9.28016 10.6133C9.28016 11.2812 8.74109 11.8203 8.07312 11.8203Z" fill="#AAAAAA"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

+10
View File
@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_538_1000)">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z" fill="#AAAAAA"/>
</g>
<defs>
<clipPath id="clip0_538_1000">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 404 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="46" height="11" viewBox="0 0 46 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.066 0.787999H10.976C11.564 0.787999 12.166 0.942 12.67 1.376C13.174 1.796 13.496 2.482 13.496 3.28L13.468 3.91H11.676V3.182C11.676 2.776 11.564 2.622 11.41 2.496C11.27 2.37 11.018 2.286 10.752 2.286H3.29C3.024 2.286 2.772 2.37 2.632 2.496C2.478 2.622 2.366 2.776 2.366 3.182V7.606C2.366 8.012 2.478 8.18 2.632 8.306C2.772 8.432 3.024 8.502 3.29 8.502H10.752C11.018 8.502 11.27 8.432 11.41 8.306C11.564 8.18 11.676 8.012 11.676 7.606C11.676 7.606 11.676 7.172 11.676 6.542H6.986V5.044H13.468L13.496 7.522C13.496 8.306 13.16 8.992 12.67 9.426C12.166 9.846 11.564 10 10.976 10H3.066C2.492 10 1.876 9.846 1.372 9.426C0.882 8.992 0.56 8.306 0.56 7.522V3.28C0.56 2.482 0.882 1.796 1.372 1.376C1.876 0.942 2.492 0.787999 3.066 0.787999ZM17.275 0.787999H24.191C24.765 0.787999 25.381 0.942 25.885 1.376C26.347 1.754 26.655 2.384 26.697 3.098H24.877C24.863 2.748 24.765 2.608 24.625 2.496C24.471 2.37 24.233 2.286 23.967 2.286H17.499C17.233 2.286 16.981 2.37 16.841 2.496C16.687 2.622 16.575 2.776 16.575 3.182V3.658C16.575 4.064 16.687 4.218 16.841 4.344C16.981 4.47 17.233 4.554 17.499 4.554H24.471C25.045 4.554 25.661 4.722 26.165 5.142C26.655 5.562 26.977 6.248 26.977 7.046V7.522C26.963 8.306 26.655 8.992 26.151 9.426C25.647 9.846 25.045 10.014 24.457 10.014H17.275C16.687 10.014 16.085 9.846 15.581 9.426C15.091 8.992 14.769 8.306 14.769 7.522V7.242H16.575C16.575 7.424 16.575 7.606 16.575 7.606C16.575 8.012 16.687 8.18 16.841 8.306C16.981 8.432 17.233 8.502 17.499 8.502H24.233C24.499 8.502 24.751 8.432 24.891 8.306C25.045 8.18 25.157 8.012 25.157 7.606C25.157 7.606 25.157 7.074 25.157 6.794C25.129 6.5 25.031 6.374 24.905 6.262C24.751 6.136 24.513 6.052 24.247 6.052H17.275C16.701 6.052 16.085 5.884 15.581 5.464C15.105 5.058 14.783 4.4 14.769 3.63V3.28C14.769 2.482 15.091 1.796 15.581 1.376C16.085 0.942 16.701 0.787999 17.275 0.787999ZM28.344 0.787999H30.15V10H28.344V0.787999ZM31.8505 0.787999H33.5165L38.7805 8.082L44.0305 0.787999H45.7105V10H43.9045V3.91L39.5365 10H38.0245L33.6565 3.91V10H31.8505V0.787999Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="74" height="20" viewBox="0 0 74 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.67944 5.44019C6.19415 5.60666 5.73957 6.04067 5.31571 6.58764C5.1007 6.86708 1.5255 12.967 1.29822 13.2821C0.425916 14.4653 -0.0348057 14.2334 0.00205204 14.5069C0.0389098 14.7804 1.4395 14.9884 2.30566 14.5842C2.76638 14.3701 3.29468 14.0015 3.69397 13.4189C3.89669 13.1275 7.47803 7.03355 7.71146 6.72439C8.60219 5.5591 9.03219 5.77313 9.00762 5.49964C8.98305 5.22615 7.55174 5.07158 6.6733 5.44019" fill="#AAAAAA"/>
<path d="M14.0505 5.44074C13.5652 5.60721 13.1107 6.04123 12.6868 6.5882C12.4718 6.86763 8.90273 12.9735 8.6693 13.2827C7.77243 14.448 7.30556 14.2399 7.37313 14.5074C7.44685 14.7988 8.8413 14.9355 9.67674 14.5847C10.1436 14.3885 10.6535 14.0021 11.065 13.4194C11.2678 13.1281 14.8491 7.0341 15.0825 6.72494C15.9733 5.55965 16.4463 5.76774 16.3787 5.5002C16.2988 5.19104 14.9228 5.06024 14.0444 5.44074" fill="#AAAAAA"/>
<path d="M7.63145 10.6726C7.2383 10.7143 6.91887 10.5597 6.73458 10.417C6.73458 10.417 6.73458 10.417 6.72844 10.417C5.83771 11.9212 5.10055 13.1697 4.98384 13.3124C4.09311 14.4777 3.65082 14.2636 3.68768 14.5371C3.73068 14.8403 5.16813 15.0068 6.00971 14.6144C6.47043 14.3944 6.97416 14.0258 7.38573 13.4491C7.47788 13.3183 8.2396 12.0639 9.09962 10.6072C9.00133 10.5597 8.87233 10.4883 8.70033 10.4646C8.22118 10.411 7.88331 10.6726 7.63145 10.6726Z" fill="#AAAAAA"/>
<path d="M10.3647 5.441C9.87944 5.60747 9.42486 6.04148 9.00099 6.58846C8.90885 6.71331 8.09798 8.08669 7.28711 9.44818C7.39768 9.54331 7.66797 9.60276 7.88298 9.60276C8.15941 9.60276 8.41127 9.45412 8.49113 9.4244C8.63856 9.37683 8.98257 9.32333 9.302 9.43629C9.41872 9.47791 9.56 9.56709 9.66443 9.62059C10.4937 8.22343 11.2985 6.86194 11.4029 6.7252C12.2998 5.55991 12.7728 5.76205 12.6991 5.50046C12.6131 5.20319 11.2309 5.04861 10.3647 5.441Z" fill="#AAAAAA"/>
<path d="M24.7522 5.55908C24.875 5.67799 24.9057 5.84446 24.8627 6.05849C25.2129 6.07633 25.563 6.07633 25.9132 6.05849C26.2572 6.04065 26.6135 6.01093 26.9882 5.96336C27.3568 5.9158 27.5411 6.12389 27.5288 6.59357C27.5226 7.05731 27.3568 7.25945 27.0373 7.2C26.8776 7.17027 26.7179 7.15244 26.5582 7.15244C26.3985 7.15244 26.2265 7.17027 26.0545 7.2C26.1343 7.29513 26.208 7.41998 26.2695 7.58645C26.3309 7.74697 26.3677 7.91939 26.3677 8.08586C26.3677 8.56743 26.1773 9.03712 25.7903 9.49491C25.4033 9.9527 24.8566 10.1846 24.1501 10.1846C23.4437 10.1846 22.9338 9.9646 22.5223 9.53058C22.1107 9.09657 21.908 8.62689 21.908 8.13342C21.908 7.96101 21.9387 7.78265 22.0062 7.59834C22.0677 7.41403 22.1353 7.27729 22.1967 7.2C22.0861 7.15244 21.9202 7.1346 21.7052 7.14055C21.4902 7.14649 21.306 7.17027 21.1647 7.2C20.8759 7.25945 20.7285 7.05731 20.7285 6.58168C20.7285 6.10605 20.8882 5.90391 21.2077 5.96336C21.5578 6.02282 21.9202 6.05849 22.295 6.07038C22.6635 6.07633 23.0444 6.07038 23.4253 6.05849C23.4068 5.82662 23.4498 5.6542 23.5604 5.54719C23.6648 5.44017 23.8614 5.38666 24.1501 5.38666C24.4389 5.38666 24.6416 5.44612 24.7644 5.55908H24.7522ZM22.7618 12.6222C22.7803 12.8957 22.7925 13.1097 22.811 13.2643C23.0997 13.3118 23.5358 13.3356 24.1256 13.3475C24.7091 13.3535 25.3726 13.3594 26.1159 13.3594C26.8592 13.3594 27.7131 13.3535 28.6345 13.3356C29.5559 13.3178 30.2194 13.2999 30.6248 13.2643C30.9934 13.2345 31.1777 13.4486 31.1777 13.9064C31.1777 14.3642 30.9934 14.5782 30.6248 14.5485C30.2255 14.5187 29.5989 14.495 28.7451 14.4771C27.8912 14.4593 27.0189 14.4533 26.1159 14.4533C25.2129 14.4533 24.4389 14.4652 23.7447 14.489C23.0506 14.5128 22.51 14.5366 22.1414 14.5723C21.8035 14.602 21.521 14.5425 21.2998 14.3879C21.0725 14.2334 21.0049 13.9539 21.0848 13.5497C21.1462 13.2405 21.1831 12.9432 21.1831 12.6638C21.1831 12.3844 21.1524 12.0811 21.0848 11.7542C21.0172 11.3974 21.3182 11.2191 21.9755 11.2191C22.6328 11.2191 22.9215 11.3974 22.8417 11.7542C22.7803 12.0633 22.7557 12.3546 22.768 12.6281L22.7618 12.6222ZM23.4744 7.31891C23.2901 7.49132 23.198 7.70536 23.198 7.9729C23.198 8.2226 23.2901 8.43664 23.4744 8.615C23.6587 8.79336 23.8799 8.88254 24.1379 8.88254C24.3959 8.88254 24.617 8.79336 24.8013 8.615C24.9856 8.43664 25.0777 8.2226 25.0777 7.9729C25.0777 7.7113 24.9856 7.49132 24.8013 7.31891C24.617 7.14649 24.3959 7.06326 24.1379 7.06326C23.8799 7.06326 23.6587 7.14649 23.4744 7.31891ZM30.8337 6.26658C30.8152 6.51628 30.8091 6.72437 30.8091 6.89679C30.8091 7.0038 30.932 7.0692 31.1715 7.08109C31.4111 7.09893 31.62 7.07515 31.7981 7.00975C32.1483 6.8849 32.3264 7.10487 32.3264 7.67563C32.3264 8.24638 32.1483 8.49014 31.7981 8.41285C31.6077 8.36529 31.3927 8.32962 31.1593 8.29395C30.9258 8.26422 30.8091 8.29989 30.8091 8.41285C30.8091 8.64472 30.8152 9.07874 30.8337 9.70894C30.8521 10.3392 30.8767 10.7791 30.9074 11.0288C30.9565 11.3736 30.6678 11.5401 30.0535 11.5401C29.4392 11.5401 29.1444 11.3677 29.1751 11.0288C29.2058 10.6721 29.2365 10.1846 29.2611 9.5722C29.2857 8.95983 29.2979 8.47825 29.2979 8.13937C29.2979 7.80048 29.2857 7.34863 29.2611 6.83139C29.2365 6.32009 29.2058 5.89202 29.1751 5.54719C29.1444 5.19047 29.4331 5.0121 30.0535 5.024C30.674 5.02994 30.9565 5.2083 30.9074 5.54719C30.8767 5.77906 30.8521 6.02282 30.8337 6.27252V6.26658Z" fill="#AAAAAA"/>
<path d="M39.6671 5.77778C39.8453 5.9502 39.9067 6.18207 39.8575 6.47933C39.8268 6.72904 39.7961 6.95496 39.7715 7.169C39.747 7.37708 39.7347 7.56734 39.7347 7.73975C39.7347 7.94189 39.747 8.13215 39.7715 8.31051C39.7961 8.48887 39.8207 8.7029 39.8575 8.95261C39.9067 9.24987 39.8391 9.51147 39.6671 9.74334C39.489 9.97521 39.2064 10.0644 38.8255 9.99899C38.5859 9.96926 38.3341 9.93954 38.0761 9.91576C37.8181 9.89197 37.5785 9.88008 37.3512 9.88008C37.1424 9.88008 36.9151 9.89197 36.6632 9.91576C36.4113 9.93954 36.1718 9.96926 35.926 9.99899C35.5698 10.0466 35.2995 9.96926 35.109 9.76712C34.9186 9.56498 34.851 9.2796 34.9186 8.90504C34.9493 8.65534 34.98 8.4413 35.0046 8.25105C35.0292 8.06675 35.0415 7.89433 35.0415 7.73975C35.0415 7.47816 35.0292 7.25818 35.0046 7.08576C34.98 6.91335 34.9493 6.72309 34.9186 6.50312C34.8695 6.24152 34.937 6.0037 35.1213 5.80156C35.3056 5.59942 35.5759 5.52213 35.926 5.56969C36.1656 5.59942 36.4113 5.62915 36.6632 5.65293C36.9151 5.67671 37.1424 5.6886 37.3512 5.6886C37.5785 5.6886 37.8242 5.67671 38.1007 5.65293C38.3709 5.62915 38.6535 5.60536 38.9422 5.56969C39.2494 5.53997 39.489 5.61131 39.6671 5.77778ZM44.8948 11.0573C45.1712 11.2535 45.2633 11.5507 45.1835 11.9491C45.1528 12.1572 45.1221 12.3831 45.0975 12.6328C45.0729 12.8825 45.0606 13.0906 45.0606 13.263C45.0606 13.4176 45.0729 13.6138 45.0975 13.8456C45.1221 14.0775 45.1466 14.2915 45.1835 14.4759C45.2633 14.8147 44.9746 14.9931 44.3173 14.999C43.66 15.005 43.359 14.8088 43.4266 14.4045C43.4573 14.2024 43.488 13.9824 43.5126 13.7505C43.5372 13.5186 43.5495 13.2987 43.5495 13.0965C43.5495 13.0014 43.5495 12.8765 43.5372 12.722C43.531 12.5674 43.4942 12.4485 43.4266 12.3712C43.3775 12.2939 42.9167 12.2523 42.0383 12.2404C41.166 12.2344 40.4657 12.2285 39.9558 12.2285C39.446 12.2285 38.6535 12.2404 37.5969 12.2642C36.5342 12.288 35.8523 12.3177 35.5513 12.3474C35.195 12.395 35.023 12.1393 35.023 11.5923C35.023 11.0454 35.2012 10.7778 35.5513 10.8135C36.0673 10.8432 36.7983 10.873 37.7566 10.8967C38.7149 10.9205 39.489 10.9324 40.0848 10.9324C40.6807 10.9324 41.4056 10.9205 42.2164 10.8967C43.0273 10.873 43.617 10.8492 43.9856 10.8135C44.3235 10.7838 44.6306 10.8611 44.9009 11.0573H44.8948ZM36.4052 7.79326C36.3991 8.1916 36.4236 8.41752 36.4912 8.48292C36.5711 8.54238 36.8413 8.58994 37.2959 8.61372C37.7505 8.6375 38.0208 8.60183 38.1007 8.5067C38.1805 8.42941 38.2235 8.18565 38.2235 7.78137C38.2235 7.37708 38.1805 7.14521 38.1007 7.07982C38.0392 7.02036 37.7689 6.97874 37.2959 6.96091C36.8229 6.94307 36.5588 6.97874 36.5158 7.05603C36.4482 7.15116 36.4175 7.39492 36.4052 7.79326ZM42.5359 6.15828C42.5174 6.33664 42.5113 6.49122 42.5113 6.61013C42.5113 6.70526 42.6833 6.75282 43.0273 6.75282C43.3713 6.75282 43.5433 6.70526 43.5433 6.61013C43.5433 6.45555 43.5372 6.28314 43.5187 6.09883C43.5003 5.91452 43.4757 5.72427 43.445 5.53997C43.3959 5.18324 43.6785 5.00488 44.2866 5.00488C44.8948 5.00488 45.1835 5.18324 45.1528 5.53997C45.122 5.92641 45.0975 6.29503 45.079 6.63391C45.0606 6.9728 45.0545 7.29979 45.0545 7.6149C45.0545 7.93 45.0606 8.26294 45.079 8.63156C45.0975 8.99422 45.122 9.40445 45.1528 9.8563C45.1835 10.2011 44.8948 10.3676 44.2743 10.3676C43.6539 10.3676 43.3652 10.1952 43.3959 9.8563C43.4266 9.52931 43.4573 9.24393 43.4942 9.00611C43.5249 8.76235 43.5433 8.49481 43.5433 8.20349C43.5433 8.10836 43.3713 8.0608 43.0273 8.0608C42.6833 8.0608 42.5113 8.10836 42.5113 8.20349C42.5113 8.4532 42.5113 8.69696 42.5236 8.94072C42.5297 9.18448 42.5543 9.4758 42.585 9.81468C42.6157 10.1595 42.3331 10.326 41.7434 10.326C41.1537 10.326 40.865 10.1536 40.9018 9.81468C40.9326 9.46985 40.9571 9.11313 40.9756 8.74452C40.9878 8.36996 41.0001 7.97162 41.0001 7.54355C41.0001 7.11549 40.994 6.74688 40.9756 6.44961C40.9571 6.15828 40.9326 5.87291 40.9018 5.59347C40.8527 5.23675 41.1353 5.05839 41.7434 5.05839C42.3516 5.05839 42.6342 5.23675 42.585 5.59347C42.5666 5.79562 42.5543 5.98587 42.5359 6.16423V6.15828Z" fill="#AAAAAA"/>
<path d="M54.1397 5.85019C54.3793 5.99288 54.4776 6.23664 54.4468 6.59336C54.4161 6.97981 54.3916 7.38409 54.3731 7.79432C54.3547 8.20455 54.3486 8.63856 54.3486 9.09041C54.3486 9.54226 54.3547 9.97627 54.3731 10.44C54.3916 10.9037 54.4161 11.3675 54.4468 11.8134C54.4776 12.1582 54.195 12.3247 53.593 12.3247C52.991 12.3247 52.7022 12.1523 52.7391 11.8134C52.7882 11.2843 52.8128 10.8027 52.8251 10.3687C52.8312 9.93465 52.8374 9.51253 52.8374 9.10825C52.8374 8.70396 52.8374 8.30562 52.8251 7.91917C52.819 7.53273 52.7698 7.29491 52.69 7.21762C52.6224 7.15817 52.4381 7.1225 52.1248 7.11061C51.8115 7.10466 51.4675 7.09871 51.0989 7.09871C50.6996 7.09871 50.3372 7.10466 50.0116 7.1225C49.6922 7.14033 49.3789 7.16411 49.0718 7.19384C48.7155 7.2414 48.5435 6.9917 48.5435 6.44472C48.5435 5.89775 48.7216 5.65399 49.0718 5.70155C49.3789 5.73128 49.7168 5.75506 50.0976 5.7729C50.4723 5.79073 50.8594 5.79668 51.2402 5.79668C51.6211 5.79668 52.0019 5.79073 52.3705 5.7729C52.7391 5.75506 53.0524 5.73128 53.3042 5.70155C53.6237 5.65399 53.9001 5.70155 54.1335 5.84424L54.1397 5.85019ZM58.6179 6.67659C58.5995 7.00953 58.5933 7.31275 58.5933 7.57434C58.5933 7.68136 58.7162 7.73487 58.9558 7.72298C59.1953 7.71703 59.4042 7.68731 59.5823 7.63974C59.9509 7.54462 60.1352 7.75865 60.1229 8.28184C60.1168 8.80503 59.9325 9.02501 59.5823 8.94772C59.3919 8.90016 59.1769 8.87043 58.9435 8.8526C58.71 8.83476 58.5933 8.88232 58.5933 8.99529C58.5933 9.89898 58.5995 10.7848 58.6179 11.6648C58.6363 12.5447 58.6609 13.4305 58.6916 14.3342C58.71 14.6731 58.4213 14.8455 57.8378 14.8455C57.2542 14.8455 56.9655 14.6731 56.9839 14.3342C57.0146 13.6327 57.0392 12.8954 57.0576 12.1166C57.076 11.3378 57.0822 10.6005 57.0822 9.89898C57.0822 9.19743 57.076 8.47209 57.0576 7.71703C57.0392 6.96197 57.0146 6.23664 56.9839 5.53508C56.9655 5.17836 57.2542 5 57.85 5C58.4459 5 58.7223 5.17836 58.6916 5.53508C58.6609 5.9572 58.6363 6.33176 58.6179 6.6647V6.67659Z" fill="#AAAAAA"/>
<path d="M74.0003 10.12C74.0003 10.5837 73.816 10.8096 73.4474 10.7977C72.612 10.768 71.752 10.7442 70.8674 10.7264C69.9828 10.7086 69.0982 10.7026 68.2136 10.7026C67.329 10.7026 66.4567 10.7086 65.5967 10.7264C64.7367 10.7442 63.8583 10.768 62.9614 10.7977C62.6051 10.8156 62.4331 10.5897 62.4331 10.1319C62.4331 9.67407 62.6113 9.4541 62.9614 9.46599C63.7661 9.49571 64.7121 9.53733 65.8056 9.58489C66.899 9.63246 67.4458 9.60868 67.4458 9.51355V9.26979C67.4458 9.19844 67.4273 9.09737 67.3966 8.95469C67.3352 8.64553 67.5993 8.49095 68.2013 8.49095C68.8033 8.49095 69.0736 8.64553 69.0061 8.95469C68.9876 9.10926 68.9754 9.22223 68.9692 9.29357C68.9631 9.36492 68.9569 9.43626 68.9569 9.51355C68.9569 9.60868 69.5405 9.63246 70.7138 9.58489C71.8871 9.53733 72.7963 9.50166 73.4351 9.46599C73.8037 9.43626 73.988 9.65029 73.988 10.12H74.0003ZM73.5641 6.19009C73.5641 6.63599 73.3799 6.84408 73.0113 6.83219C72.5628 6.80246 71.8687 6.76084 70.9411 6.71328C70.0074 6.66572 69.6449 6.67166 69.8538 6.73706C70.4005 6.89164 70.935 7.04028 71.4571 7.18297C71.9793 7.32565 72.4891 7.4624 72.9867 7.60509C73.3737 7.7121 73.4904 7.9737 73.3491 8.38393C73.2017 8.79416 72.9744 8.94874 72.6488 8.83578C71.9117 8.60391 71.021 8.33637 69.9767 8.04504C68.9324 7.74778 68.3303 7.59914 68.1706 7.59914C68.0109 7.59914 67.4212 7.74778 66.4015 8.05099C65.3817 8.3542 64.5094 8.62174 63.7846 8.85361C63.4651 8.96063 63.2194 8.81794 63.0474 8.4196C62.8754 8.02126 63.0044 7.76561 63.4221 7.64076C63.9688 7.46834 64.5094 7.30782 65.05 7.16513C65.5906 7.0165 66.0882 6.87381 66.555 6.73112C66.7823 6.65383 66.4322 6.64194 65.5046 6.69545C64.5831 6.74895 63.8644 6.79057 63.3484 6.82624C62.9921 6.84408 62.8201 6.63005 62.8201 6.18414C62.8201 5.73824 62.9983 5.53015 63.3484 5.54204C64.1347 5.57177 64.9394 5.59555 65.7564 5.61339C66.5735 5.63123 67.3782 5.63717 68.1645 5.63717C68.9508 5.63717 69.7617 5.63123 70.5848 5.61339C71.4141 5.59555 72.2188 5.57177 73.0051 5.54204C73.3737 5.52421 73.558 5.73824 73.558 6.18414L73.5641 6.19009ZM73.0973 11.4042C73.3614 11.5944 73.4536 11.8798 73.3737 12.2663C73.343 12.4684 73.3123 12.6468 73.2877 12.8132C73.2631 12.9797 73.2508 13.1462 73.2508 13.3126C73.2508 13.4672 73.2631 13.6456 73.2877 13.8477C73.3123 14.0499 73.3369 14.2461 73.3737 14.4304C73.4536 14.7693 73.171 14.9476 72.5198 14.9536C71.8687 14.9595 71.5738 14.7633 71.6414 14.359C71.6721 14.1569 71.6967 13.9845 71.7151 13.8358C71.7335 13.6872 71.7397 13.5148 71.7397 13.3126C71.7397 13.2175 71.7397 13.1164 71.7274 13.0094C71.7213 12.9024 71.6844 12.8073 71.6168 12.73C71.5677 12.6527 71.1192 12.6111 70.2654 12.5992C69.4115 12.5932 68.7296 12.5873 68.2198 12.5873C67.7099 12.5873 66.8437 12.5992 65.6274 12.623C64.4173 12.6468 63.6556 12.6765 63.3484 12.7062C62.9921 12.7538 62.8201 12.4981 62.8201 11.9511C62.8201 11.4042 62.9983 11.1366 63.3484 11.1723C63.8644 11.202 64.6753 11.2318 65.781 11.2555C66.8868 11.2793 67.7406 11.2912 68.3365 11.2912C68.9324 11.2912 69.6572 11.2793 70.4681 11.2555C71.279 11.2318 71.8625 11.208 72.2127 11.1723C72.5321 11.1426 72.827 11.2199 73.0911 11.4101L73.0973 11.4042Z" fill="#AAAAAA"/>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 8V6.8H10.5V8H5.5ZM5.5 5.6V4.4H10.5V5.6H5.5ZM4.25 9.2H8.9375C9.23958 9.2 9.52083 9.2624 9.78125 9.3872C10.0417 9.5124 10.2604 9.69 10.4375 9.92L11.75 11.57V3.2H4.25V9.2ZM4.25 12.8H11.1562L9.45312 10.655C9.39062 10.575 9.31521 10.5126 9.22688 10.4678C9.13813 10.4226 9.04167 10.4 8.9375 10.4H4.25V12.8ZM11.75 14H4.25C3.90625 14 3.61208 13.8826 3.3675 13.6478C3.1225 13.4126 3 13.13 3 12.8V3.2C3 2.87 3.1225 2.5874 3.3675 2.3522C3.61208 2.1174 3.90625 2 4.25 2H11.75C12.0938 2 12.3881 2.1174 12.6331 2.3522C12.8777 2.5874 13 2.87 13 3.2V12.8C13 13.13 12.8777 13.4126 12.6331 13.6478C12.3881 13.8826 12.0938 14 11.75 14Z" fill="#8FA8A4"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.33301 12.6673V8.66732H3.33301V7.33399H7.33301V3.33398H8.66634V7.33399H12.6663V8.66732H8.66634V12.6673H7.33301Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 242 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.99967 9.66732C2.53301 9.66732 2.13856 9.50621 1.81634 9.18398C1.49412 8.86176 1.33301 8.46732 1.33301 8.00065C1.33301 7.53398 1.49412 7.13954 1.81634 6.81732C2.13856 6.4951 2.53301 6.33398 2.99967 6.33398C3.34412 6.33398 3.65523 6.42843 3.93301 6.61732C4.21079 6.80621 4.41079 7.0451 4.53301 7.33398H14.6663V8.66732H4.53301C4.41079 8.95621 4.21079 9.1951 3.93301 9.38398C3.65523 9.57287 3.34412 9.66732 2.99967 9.66732Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 551 B

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1486_25703)">
<path d="M11.3335 2H4.66683C3.9335 2 3.34016 2.6 3.34016 3.33333L3.3335 14L8.00016 12L12.6668 14V3.33333C12.6668 2.6 12.0668 2 11.3335 2ZM11.3335 12L8.00016 10.5467L4.66683 12V3.33333H11.3335V12Z" fill="#8FA8A4"/>
</g>
<defs>
<clipPath id="clip0_1486_25703">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 465 B

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1486_25699)">
<path d="M11.3335 2H4.66683C3.9335 2 3.34016 2.6 3.34016 3.33333L3.3335 14L8.00016 12L12.6668 14V3.33333C12.6668 2.6 12.0668 2 11.3335 2Z" fill="#1E5149"/>
</g>
<defs>
<clipPath id="clip0_1486_25699">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 407 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667V10H3.33333V12.6667H6V14H3.33333ZM10 14V12.6667H12.6667V10H14V12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14H10ZM2 6V3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13056 2.96667 2 3.33333 2H6V3.33333H3.33333V6H2ZM12.6667 6V3.33333H10V2H12.6667C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333V6H12.6667Z" fill="white"/>
<path d="M9.6416 6.36719H11.3003C11.4176 6.36719 11.5278 6.39208 11.6309 6.44185C11.7376 6.49163 11.83 6.56097 11.9083 6.64985C11.9865 6.73874 12.0469 6.84185 12.0896 6.95919C12.1358 7.07297 12.1589 7.19563 12.1589 7.32719V9.03919C12.1589 9.1743 12.1358 9.29874 12.0896 9.41252C12.0469 9.5263 11.9865 9.62763 11.9083 9.71652C11.83 9.80541 11.7376 9.87474 11.6309 9.92452C11.5278 9.9743 11.4176 9.99919 11.3003 9.99919H9.6416V6.36719ZM11.1349 9.32719C11.1883 9.32719 11.2363 9.31652 11.2789 9.29519C11.3216 9.27385 11.3589 9.24541 11.3909 9.20985C11.4265 9.1743 11.4532 9.13341 11.4709 9.08719C11.4887 9.03741 11.4976 8.98408 11.4976 8.92719V7.43385C11.4976 7.38052 11.4887 7.33074 11.4709 7.28452C11.4532 7.23474 11.4265 7.19208 11.3909 7.15652C11.3589 7.11741 11.3216 7.08719 11.2789 7.06585C11.2363 7.04452 11.1883 7.03385 11.1349 7.03385H10.3083V9.32719H11.1349Z" fill="white"/>
<path d="M8.41482 9.44452H7.43882L7.30549 9.99919H6.66016L7.59349 6.36719H8.28682L9.18282 9.99919H8.54282L8.41482 9.44452ZM7.56682 8.90585H8.29216L7.95616 7.42319H7.91882L7.56682 8.90585Z" fill="white"/>
<path d="M5.3331 9.36985C5.43621 9.36985 5.51088 9.33963 5.5571 9.27919C5.60688 9.21519 5.64421 9.13874 5.6691 9.04985H6.3411C6.32688 9.18141 6.2931 9.30585 6.23977 9.42319C6.18643 9.53697 6.11532 9.63652 6.02643 9.72185C5.9411 9.80719 5.83799 9.87474 5.7171 9.92452C5.59977 9.9743 5.47355 9.99919 5.33844 9.99919H4.87977C4.73755 9.99919 4.60243 9.97252 4.47443 9.91919C4.34643 9.8623 4.23443 9.78585 4.13843 9.68985C4.04243 9.59385 3.96599 9.48185 3.9091 9.35385C3.85577 9.22585 3.8291 9.08897 3.8291 8.94319V7.42852C3.8291 7.28274 3.85577 7.14585 3.9091 7.01785C3.96599 6.8863 4.04243 6.77252 4.13843 6.67652C4.23443 6.58052 4.34643 6.50585 4.47443 6.45252C4.60243 6.39563 4.73755 6.36719 4.87977 6.36719H5.33844C5.4771 6.36719 5.6051 6.39208 5.72243 6.44185C5.83977 6.49163 5.9411 6.56097 6.02643 6.64985C6.11532 6.73519 6.18643 6.83652 6.23977 6.95385C6.2931 7.07119 6.32688 7.19741 6.3411 7.33252H5.6691C5.65132 7.24008 5.61755 7.16185 5.56777 7.09785C5.51799 7.03385 5.43977 7.00185 5.3331 7.00185H4.8851C4.76066 7.00185 4.6611 7.0463 4.58644 7.13519C4.51532 7.22408 4.47977 7.33252 4.47977 7.46052V8.90585C4.47977 9.03385 4.51532 9.14408 4.58644 9.23652C4.6611 9.32541 4.76066 9.36985 4.8851 9.36985H5.3331Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1507_347)">
<path d="M12.6667 2.66732H12V1.33398H10.6667V2.66732H5.33333V1.33398H4V2.66732H3.33333C2.59333 2.66732 2.00667 3.26732 2.00667 4.00065L2 13.334C2 14.0673 2.59333 14.6673 3.33333 14.6673H12.6667C13.4 14.6673 14 14.0673 14 13.334V4.00065C14 3.26732 13.4 2.66732 12.6667 2.66732ZM12.6667 13.334H3.33333V6.66732H12.6667V13.334ZM6 9.33398H4.66667V8.00065H6V9.33398ZM8.66667 9.33398H7.33333V8.00065H8.66667V9.33398ZM11.3333 9.33398H10V8.00065H11.3333V9.33398ZM6 12.0007H4.66667V10.6673H6V12.0007ZM8.66667 12.0007H7.33333V10.6673H8.66667V12.0007ZM11.3333 12.0007H10V10.6673H11.3333V12.0007Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1507_347">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 847 B

+10
View File
@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_689_1178)">
<path d="M6 13.9998H4.66667V12.6665H6V13.9998ZM3.33333 5.99984H2V4.6665H3.33333V5.99984ZM14 3.33317V12.6665C14 13.3998 13.4 13.9998 12.6667 13.9998H10V12.6665V3.33317V1.99984H12.6667C13.4 1.99984 14 2.59984 14 3.33317ZM3.33333 1.99984V3.33317H2C2 2.59984 2.6 1.99984 3.33333 1.99984ZM8.66667 15.3332H7.33333V0.666504H8.66667V15.3332ZM3.33333 11.3332H2V9.99984H3.33333V11.3332ZM6 3.33317H4.66667V1.99984H6V3.33317ZM3.33333 8.6665H2V7.33317H3.33333V8.6665ZM3.33333 13.9998C2.6 13.9998 2 13.3998 2 12.6665H3.33333V13.9998Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_689_1178">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 783 B

Some files were not shown because too many files have changed in this diff Show More