Compare commits
26 Commits
8efb5da65f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb5b0f00c2 | ||
|
|
637b390024 | ||
|
|
4b4ffafbd2 | ||
|
|
1cd0f21a36 | ||
|
|
f77be3f482 | ||
|
|
2e8c79bb43 | ||
|
|
8121c9cf41 | ||
|
|
e67fd41cbf | ||
|
|
c9a93ea936 | ||
|
|
8d0cc78abc | ||
|
|
bbebe24763 | ||
|
|
2053791589 | ||
|
|
fc23156b2c | ||
|
|
33f157cb08 | ||
|
|
b735a4cdd1 | ||
|
|
6e55b99e9a | ||
|
|
cbae8769bf | ||
|
|
bc60f932c3 | ||
|
|
ca57a4a1e4 | ||
|
|
e50b24c25b | ||
|
|
24852d4401 | ||
|
|
d66614123e | ||
|
|
1d15cf9b9b | ||
|
|
61b5638cb1 | ||
|
|
baf6019c1c | ||
|
|
69a14fab51 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -8,4 +8,12 @@ backend/data/
|
||||
uploads/
|
||||
snapshots/
|
||||
node_modules/
|
||||
incoming-files/*.Zone.Identifier
|
||||
*:Zone.Identifier
|
||||
incoming-files/~$*
|
||||
|
||||
# Local-only inspection / conversion artifacts
|
||||
incoming-files/6f.html
|
||||
incoming-files/7f.html
|
||||
incoming-files/center.html
|
||||
.dev-worktree-8081/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -8,8 +8,8 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;600;700;900&display=swap" rel="stylesheet" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="/legacy/static/common.css?v=20260325-9" />
|
||||
<link rel="stylesheet" href="/legacy/static/organization.css?v=20260325-9" />
|
||||
<link rel="stylesheet" href="/legacy/static/common.css?v=20260331-01" />
|
||||
<link rel="stylesheet" href="/legacy/static/organization.css?v=20260331-01" />
|
||||
</head>
|
||||
<body>
|
||||
<input type="file" id="upload-excel" class="hidden" accept=".xlsx, .csv" />
|
||||
@@ -60,6 +60,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/legacy/static/organization.js?v=20260325-9"></script>
|
||||
</body>
|
||||
<script src="/legacy/static/organization.js?v=20260331-01"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,7 +11,6 @@ RUN pip install --no-cache-dir -r /app/requirements.txt
|
||||
COPY backend/app /app/backend/app
|
||||
COPY DashBoard-organization.html /app/legacy/DashBoard-organization.html
|
||||
COPY DashBoard-organization-backup.html /app/legacy/DashBoard-organization-backup.html
|
||||
COPY organization1.xlsx /app/legacy/organization1.xlsx
|
||||
COPY legacy/static /app/legacy/static
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
@@ -32,6 +32,48 @@ CREATE TABLE IF NOT EXISTS members (
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_overrides (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
company TEXT,
|
||||
rank TEXT,
|
||||
role TEXT,
|
||||
department TEXT,
|
||||
grp TEXT,
|
||||
division TEXT,
|
||||
team TEXT,
|
||||
cell TEXT,
|
||||
work_status TEXT,
|
||||
work_time TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
seat_label TEXT,
|
||||
photo_url TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_retirements (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_aliases (
|
||||
id SERIAL PRIMARY KEY,
|
||||
alias_name TEXT NOT NULL UNIQUE,
|
||||
canonical_name TEXT NOT NULL,
|
||||
employee_id TEXT,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS seat_maps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -76,11 +118,312 @@ CREATE TABLE IF NOT EXISTS seat_slots (
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (seat_map_id, slot_key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_import_batches (
|
||||
id SERIAL PRIMARY KEY,
|
||||
source_key TEXT NOT NULL UNIQUE,
|
||||
source_name TEXT NOT NULL,
|
||||
source_path TEXT NOT NULL,
|
||||
imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
row_count INTEGER NOT NULL DEFAULT 0,
|
||||
meta_json JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_raw_organization_rows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
batch_id INTEGER NOT NULL REFERENCES integration_import_batches(id) ON DELETE CASCADE,
|
||||
row_index INTEGER NOT NULL,
|
||||
row_json JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_raw_mh_rows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
batch_id INTEGER NOT NULL REFERENCES integration_import_batches(id) ON DELETE CASCADE,
|
||||
row_index INTEGER NOT NULL,
|
||||
row_json JSONB NOT NULL,
|
||||
row_values_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_raw_mh_pm_rows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
batch_id INTEGER NOT NULL REFERENCES integration_import_batches(id) ON DELETE CASCADE,
|
||||
row_index INTEGER NOT NULL,
|
||||
row_values_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_raw_payment_rows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
batch_id INTEGER NOT NULL REFERENCES integration_import_batches(id) ON DELETE CASCADE,
|
||||
row_index INTEGER NOT NULL,
|
||||
row_json JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_projects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
project_code TEXT NOT NULL UNIQUE,
|
||||
project_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
intranet_name TEXT NOT NULL DEFAULT '',
|
||||
business_area TEXT NOT NULL DEFAULT '',
|
||||
business_subarea TEXT NOT NULL DEFAULT '',
|
||||
project_nature TEXT NOT NULL DEFAULT '',
|
||||
main_category TEXT NOT NULL DEFAULT '',
|
||||
middle_category TEXT NOT NULL DEFAULT '',
|
||||
sub_category TEXT NOT NULL DEFAULT '',
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_project_aliases (
|
||||
id SERIAL PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL REFERENCES integration_projects(id) ON DELETE CASCADE,
|
||||
alias_name TEXT NOT NULL,
|
||||
alias_type TEXT NOT NULL DEFAULT 'name',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (project_id, alias_name, alias_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_project_category_mappings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
source_key TEXT NOT NULL DEFAULT 'ptj_csv',
|
||||
project_name TEXT NOT NULL,
|
||||
normalized_project_key TEXT NOT NULL,
|
||||
mapped_d1 TEXT NOT NULL DEFAULT '',
|
||||
mapped_d2 TEXT NOT NULL DEFAULT '',
|
||||
mapped_d3 TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (source_key, normalized_project_key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_project_pm_assignments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL REFERENCES integration_projects(id) ON DELETE CASCADE,
|
||||
member_id INTEGER REFERENCES members(id) ON DELETE SET NULL,
|
||||
pm_name TEXT NOT NULL,
|
||||
source_label TEXT NOT NULL DEFAULT 'mh_sheet2',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (project_id, source_label)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_work_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
work_date DATE NOT NULL,
|
||||
employee_id TEXT NOT NULL,
|
||||
member_id INTEGER REFERENCES members(id) ON DELETE SET NULL,
|
||||
member_name TEXT NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
team_category TEXT NOT NULL DEFAULT '',
|
||||
team_name TEXT NOT NULL DEFAULT '',
|
||||
user_state TEXT NOT NULL DEFAULT '',
|
||||
shift_hours NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
||||
weekend_late_flag TEXT NOT NULL DEFAULT '',
|
||||
review_status TEXT NOT NULL DEFAULT '',
|
||||
source_row_index INTEGER NOT NULL DEFAULT 0,
|
||||
raw_batch_id INTEGER REFERENCES integration_import_batches(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (work_date, employee_id, source_row_index)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_work_log_segments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
work_log_id INTEGER NOT NULL REFERENCES integration_work_logs(id) ON DELETE CASCADE,
|
||||
slot_name TEXT NOT NULL,
|
||||
project_id INTEGER REFERENCES integration_projects(id) ON DELETE SET NULL,
|
||||
project_code TEXT NOT NULL DEFAULT '',
|
||||
project_name TEXT NOT NULL DEFAULT '',
|
||||
business_type TEXT NOT NULL DEFAULT '',
|
||||
activity_code TEXT NOT NULL DEFAULT '',
|
||||
hours NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
||||
overtime_hours_raw NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
||||
overtime_hours_adjusted NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
||||
is_overtime BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integration_vouchers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
accounting_company TEXT NOT NULL DEFAULT '',
|
||||
claim_date DATE,
|
||||
issue_date DATE,
|
||||
issue_month TEXT NOT NULL DEFAULT '',
|
||||
account_code TEXT NOT NULL DEFAULT '',
|
||||
management_account_code TEXT NOT NULL DEFAULT '',
|
||||
account_name TEXT NOT NULL DEFAULT '',
|
||||
project_id INTEGER REFERENCES integration_projects(id) ON DELETE SET NULL,
|
||||
project_code TEXT NOT NULL DEFAULT '',
|
||||
project_name TEXT NOT NULL DEFAULT '',
|
||||
display_project_name TEXT NOT NULL DEFAULT '',
|
||||
intranet_project_name TEXT NOT NULL DEFAULT '',
|
||||
business_area TEXT NOT NULL DEFAULT '',
|
||||
business_subarea TEXT NOT NULL DEFAULT '',
|
||||
planning_dev_sales TEXT NOT NULL DEFAULT '',
|
||||
main_category TEXT NOT NULL DEFAULT '',
|
||||
middle_category TEXT NOT NULL DEFAULT '',
|
||||
sub_category TEXT NOT NULL DEFAULT '',
|
||||
department_name TEXT NOT NULL DEFAULT '',
|
||||
team_name TEXT NOT NULL DEFAULT '',
|
||||
customer_name TEXT NOT NULL DEFAULT '',
|
||||
summary_text TEXT NOT NULL DEFAULT '',
|
||||
debit_supply_amount NUMERIC(14, 2) NOT NULL DEFAULT 0,
|
||||
credit_supply_amount NUMERIC(14, 2) NOT NULL DEFAULT 0,
|
||||
expense_amount NUMERIC(14, 2) NOT NULL DEFAULT 0,
|
||||
income_amount NUMERIC(14, 2) NOT NULL DEFAULT 0,
|
||||
voucher_type TEXT NOT NULL DEFAULT '',
|
||||
project_nature TEXT NOT NULL DEFAULT '',
|
||||
raw_batch_id INTEGER REFERENCES integration_import_batches(id) ON DELETE SET NULL,
|
||||
source_row_index INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS history_revisions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scope TEXT NOT NULL DEFAULT 'organization',
|
||||
revision_label TEXT NOT NULL,
|
||||
created_by_user_id BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
note TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
company TEXT NOT NULL DEFAULT '',
|
||||
rank TEXT NOT NULL DEFAULT '',
|
||||
role TEXT NOT NULL DEFAULT '',
|
||||
department TEXT NOT NULL DEFAULT '',
|
||||
grp TEXT NOT NULL DEFAULT '',
|
||||
division TEXT NOT NULL DEFAULT '',
|
||||
team TEXT NOT NULL DEFAULT '',
|
||||
cell TEXT NOT NULL DEFAULT '',
|
||||
work_status TEXT NOT NULL DEFAULT '',
|
||||
work_time TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
photo_url TEXT NOT NULL DEFAULT '',
|
||||
valid_from TIMESTAMPTZ NOT NULL,
|
||||
valid_to TIMESTAMPTZ,
|
||||
revision_no BIGINT NOT NULL,
|
||||
changed_by_user_id BIGINT,
|
||||
change_reason TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS seat_assignment_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
||||
seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE,
|
||||
seat_slot_id INTEGER REFERENCES seat_slots(id) ON DELETE CASCADE,
|
||||
seat_label TEXT NOT NULL DEFAULT '',
|
||||
valid_from TIMESTAMPTZ NOT NULL,
|
||||
valid_to TIMESTAMPTZ,
|
||||
revision_no BIGINT NOT NULL,
|
||||
changed_by_user_id BIGINT,
|
||||
change_reason TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entity_change_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
revision_no BIGINT NOT NULL,
|
||||
changed_by_user_id BIGINT,
|
||||
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
change_reason TEXT NOT NULL DEFAULT '',
|
||||
patch_json JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS auth;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'admin',
|
||||
member_id INTEGER NULL REFERENCES members(id) ON DELETE SET NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_from TEXT NOT NULL DEFAULT 'manual',
|
||||
last_login_at TIMESTAMPTZ,
|
||||
password_changed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.sessions (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.login_audit_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
user_id BIGINT NULL REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
failure_reason TEXT,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
"""
|
||||
|
||||
MIGRATION_SQL = """
|
||||
ALTER TABLE members ADD COLUMN IF NOT EXISTS employee_id TEXT;
|
||||
ALTER TABLE members ADD COLUMN IF NOT EXISTS sort_order INTEGER NOT NULL DEFAULT 0;
|
||||
CREATE TABLE IF NOT EXISTS member_overrides (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
company TEXT,
|
||||
rank TEXT,
|
||||
role TEXT,
|
||||
department TEXT,
|
||||
grp TEXT,
|
||||
division TEXT,
|
||||
team TEXT,
|
||||
cell TEXT,
|
||||
work_status TEXT,
|
||||
work_time TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
seat_label TEXT,
|
||||
photo_url TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_retirements (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_aliases (
|
||||
id SERIAL PRIMARY KEY,
|
||||
alias_name TEXT NOT NULL UNIQUE,
|
||||
canonical_name TEXT NOT NULL,
|
||||
employee_id TEXT,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_slot_id INTEGER;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS row_index INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -136,14 +479,73 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'integration_raw_mh_rows' AND column_name = 'row_values_json'
|
||||
) THEN
|
||||
ALTER TABLE integration_raw_mh_rows
|
||||
ADD COLUMN row_values_json JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DROP INDEX IF EXISTS seat_positions_map_cell_idx;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS seat_positions_map_cell_idx
|
||||
ON seat_positions (seat_map_id, row_index, col_index)
|
||||
WHERE seat_map_id IS NOT NULL;
|
||||
WHERE seat_map_id IS NOT NULL
|
||||
AND seat_slot_id IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS member_overrides_employee_id_idx
|
||||
ON member_overrides (employee_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS member_retirements_name_idx
|
||||
ON member_retirements (name);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS member_aliases_alias_name_idx
|
||||
ON member_aliases (alias_name);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS seat_positions_slot_idx
|
||||
ON seat_positions (seat_slot_id)
|
||||
WHERE seat_slot_id IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS integration_raw_organization_rows_batch_row_idx
|
||||
ON integration_raw_organization_rows (batch_id, row_index);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS integration_raw_mh_rows_batch_row_idx
|
||||
ON integration_raw_mh_rows (batch_id, row_index);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS integration_raw_mh_pm_rows_batch_row_idx
|
||||
ON integration_raw_mh_pm_rows (batch_id, row_index);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS integration_raw_payment_rows_batch_row_idx
|
||||
ON integration_raw_payment_rows (batch_id, row_index);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS integration_work_logs_employee_idx
|
||||
ON integration_work_logs (employee_id, work_date);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS integration_work_log_segments_project_idx
|
||||
ON integration_work_log_segments (project_code, project_name);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS integration_vouchers_project_idx
|
||||
ON integration_vouchers (project_code, project_name);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS integration_project_category_mappings_key_idx
|
||||
ON integration_project_category_mappings (source_key, normalized_project_key);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS member_versions_member_time_idx
|
||||
ON member_versions (member_id, valid_from, valid_to);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS seat_assignment_versions_member_time_idx
|
||||
ON seat_assignment_versions (member_id, valid_from, valid_to);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS history_revisions_scope_created_idx
|
||||
ON history_revisions (scope, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS entity_change_events_entity_idx
|
||||
ON entity_change_events (entity_type, entity_id, changed_at DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
@@ -157,6 +559,58 @@ BEGIN
|
||||
FOREIGN KEY (seat_slot_id) REFERENCES seat_slots(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS auth;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'admin',
|
||||
member_id INTEGER NULL REFERENCES members(id) ON DELETE SET NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_from TEXT NOT NULL DEFAULT 'manual',
|
||||
last_login_at TIMESTAMPTZ,
|
||||
password_changed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.sessions (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.login_audit_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
user_id BIGINT NULL REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
failure_reason TEXT,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT 'admin';
|
||||
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS member_id INTEGER NULL REFERENCES members(id) ON DELETE SET NULL;
|
||||
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS created_from TEXT NOT NULL DEFAULT 'manual';
|
||||
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ;
|
||||
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS password_changed_at TIMESTAMPTZ;
|
||||
ALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS revoked_at TIMESTAMPTZ;
|
||||
ALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS ip_address INET;
|
||||
ALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS user_agent TEXT;
|
||||
ALTER TABLE auth.login_audit_logs ADD COLUMN IF NOT EXISTS user_id BIGINT NULL REFERENCES auth.users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE auth.login_audit_logs ADD COLUMN IF NOT EXISTS failure_reason TEXT;
|
||||
ALTER TABLE auth.login_audit_logs ADD COLUMN IF NOT EXISTS ip_address INET;
|
||||
ALTER TABLE auth.login_audit_logs ADD COLUMN IF NOT EXISTS user_agent TEXT;
|
||||
"""
|
||||
|
||||
|
||||
@@ -174,6 +628,7 @@ def init_db(max_retries: int = 20, retry_delay: float = 2.0) -> None:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(SCHEMA_SQL)
|
||||
cur.execute(MIGRATION_SQL)
|
||||
ensure_history_backfill(cur)
|
||||
conn.commit()
|
||||
return
|
||||
except psycopg.OperationalError as exc:
|
||||
@@ -181,3 +636,89 @@ def init_db(max_retries: int = 20, retry_delay: float = 2.0) -> None:
|
||||
time.sleep(retry_delay)
|
||||
if last_error is not None:
|
||||
raise last_error
|
||||
|
||||
|
||||
def ensure_history_backfill(cur) -> None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id
|
||||
FROM history_revisions
|
||||
WHERE scope = 'organization'
|
||||
AND revision_label = 'initial-backfill'
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO history_revisions (scope, revision_label, note)
|
||||
VALUES ('organization', 'initial-backfill', 'Seeded from current members and seat_positions state')
|
||||
RETURNING id
|
||||
"""
|
||||
)
|
||||
revision_id = int(cur.fetchone()["id"])
|
||||
else:
|
||||
revision_id = int(row["id"])
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO member_versions (
|
||||
member_id, name, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, photo_url,
|
||||
valid_from, valid_to, revision_no, changed_by_user_id, change_reason
|
||||
)
|
||||
SELECT
|
||||
m.id, m.name, COALESCE(m.company, ''), COALESCE(m.rank, ''), COALESCE(m.role, ''),
|
||||
COALESCE(m.department, ''), COALESCE(m.grp, ''), COALESCE(m.division, ''), COALESCE(m.team, ''), COALESCE(m.cell, ''),
|
||||
COALESCE(m.work_status, ''), COALESCE(m.work_time, ''), COALESCE(m.phone, ''), COALESCE(m.email, ''), COALESCE(m.photo_url, ''),
|
||||
TIMESTAMPTZ '1970-01-01 00:00:00+00', NULL, %s, NULL, 'initial-backfill'
|
||||
FROM members AS m
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM member_versions mv
|
||||
WHERE mv.member_id = m.id
|
||||
)
|
||||
""",
|
||||
(revision_id,),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO seat_assignment_versions (
|
||||
member_id, seat_map_id, seat_slot_id, seat_label,
|
||||
valid_from, valid_to, revision_no, changed_by_user_id, change_reason
|
||||
)
|
||||
SELECT
|
||||
sp.member_id, sp.seat_map_id, sp.seat_slot_id, COALESCE(sp.seat_label, ''),
|
||||
TIMESTAMPTZ '1970-01-01 00:00:00+00', NULL, %s, NULL, 'initial-backfill'
|
||||
FROM seat_positions AS sp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM seat_assignment_versions sav
|
||||
WHERE sav.member_id = sp.member_id
|
||||
)
|
||||
""",
|
||||
(revision_id,),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE member_versions
|
||||
SET valid_from = TIMESTAMPTZ '1970-01-01 00:00:00+00'
|
||||
WHERE revision_no = %s
|
||||
AND change_reason = 'initial-backfill'
|
||||
""",
|
||||
(revision_id,),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE seat_assignment_versions
|
||||
SET valid_from = TIMESTAMPTZ '1970-01-01 00:00:00+00'
|
||||
WHERE revision_no = %s
|
||||
AND change_reason = 'initial-backfill'
|
||||
""",
|
||||
(revision_id,),
|
||||
)
|
||||
|
||||
3019
backend/app/main.py
3019
backend/app/main.py
File diff suppressed because it is too large
Load Diff
521474
center2.dxf
521474
center2.dxf
File diff suppressed because it is too large
Load Diff
564192
center3.dxf
564192
center3.dxf
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
78
docker-compose.8081.yml
Normal file
78
docker-compose.8081.yml
Normal file
@@ -0,0 +1,78 @@
|
||||
services:
|
||||
proxy:
|
||||
image: nginx:1.27-alpine
|
||||
depends_on:
|
||||
frontend:
|
||||
condition: service_healthy
|
||||
backend:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8081:80"
|
||||
volumes:
|
||||
- ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1/ || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: frontend/Dockerfile
|
||||
volumes:
|
||||
- ./frontend/public:/usr/share/nginx/html:ro
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1/ || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
command: uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./backend/app:/app/backend/app:ro
|
||||
- ./DashBoard-organization.html:/app/legacy/DashBoard-organization.html:ro
|
||||
- ./DashBoard-organization-backup.html:/app/legacy/DashBoard-organization-backup.html:ro
|
||||
- ./legacy/static:/app/legacy/static:ro
|
||||
- ./incoming-files:/app/incoming-files:ro
|
||||
- uploads_data:/data/uploads
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health')\" || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 20s
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
uploads_data:
|
||||
@@ -47,6 +47,7 @@ services:
|
||||
- ./DashBoard-organization.html:/app/legacy/DashBoard-organization.html:ro
|
||||
- ./DashBoard-organization-backup.html:/app/legacy/DashBoard-organization-backup.html:ro
|
||||
- ./legacy/static:/app/legacy/static:ro
|
||||
- ./incoming-files:/app/incoming-files:ro
|
||||
- uploads_data:/data/uploads
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
|
||||
359
docs/AUTH_DB_DESIGN.md
Normal file
359
docs/AUTH_DB_DESIGN.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# Auth DB Design
|
||||
|
||||
## Goal
|
||||
|
||||
현재 조직도 업무 데이터와 로그인 데이터를 분리한다.
|
||||
|
||||
분리 원칙:
|
||||
- 업무 데이터는 기존 `public.members`, `seat_maps`, `seat_positions` 중심으로 유지
|
||||
- 인증/권한 데이터는 별도 `auth` 스키마로 분리
|
||||
- 로그인 사용자는 필요할 때만 `members.id` 와 연결
|
||||
|
||||
이 방식이면 비밀번호, 세션, 감사로그를 업무 데이터와 분리해서 관리할 수 있고,
|
||||
엑셀 임포트로 `members` 가 갱신돼도 인증 체계가 직접 흔들리지 않는다.
|
||||
|
||||
## Scope
|
||||
|
||||
이번 설계의 대상:
|
||||
- 사용자 계정
|
||||
- 비밀번호 해시
|
||||
- 세션
|
||||
- 역할과 권한
|
||||
- 로그인 감사로그
|
||||
- 사용자와 조직 구성원 연결
|
||||
|
||||
이번 설계에서 제외:
|
||||
- SSO 연동
|
||||
- OAuth/OpenID Connect
|
||||
- MFA
|
||||
- 비밀번호 재설정 메일 발송
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
권장 구조는 "같은 PostgreSQL, 다른 스키마" 이다.
|
||||
|
||||
- 업무 스키마: `public`
|
||||
- 인증 스키마: `auth`
|
||||
|
||||
초기 운영에서는 DB 인스턴스를 분리하지 않아도 된다.
|
||||
대신 아래 원칙은 바로 적용한다.
|
||||
|
||||
- 애플리케이션 계정도 가능하면 읽기/쓰기 범위를 분리
|
||||
- 인증 관련 쿼리는 `auth.*` 만 접근
|
||||
- 업무 API 는 `public.*` 중심으로 접근
|
||||
|
||||
## Core Tables
|
||||
|
||||
### `auth.users`
|
||||
|
||||
로그인 가능한 계정의 기준 테이블.
|
||||
|
||||
주요 컬럼:
|
||||
- `id BIGSERIAL PRIMARY KEY`
|
||||
- `username TEXT NOT NULL UNIQUE`
|
||||
- `password_hash TEXT NOT NULL`
|
||||
- `display_name TEXT NOT NULL`
|
||||
- `email TEXT`
|
||||
- `status TEXT NOT NULL DEFAULT 'active'`
|
||||
- `member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULL`
|
||||
- `last_login_at TIMESTAMPTZ`
|
||||
- `password_changed_at TIMESTAMPTZ`
|
||||
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
||||
- `updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
||||
|
||||
상태값 권장:
|
||||
- `active`
|
||||
- `locked`
|
||||
- `disabled`
|
||||
|
||||
원칙:
|
||||
- `username` 는 로그인 식별자
|
||||
- `member_id` 는 선택 연결
|
||||
- 구성원이 퇴사하거나 엑셀에서 빠져도 계정 자체는 바로 삭제하지 않음
|
||||
|
||||
### `auth.roles`
|
||||
|
||||
권한 묶음 정의.
|
||||
|
||||
주요 컬럼:
|
||||
- `id BIGSERIAL PRIMARY KEY`
|
||||
- `code TEXT NOT NULL UNIQUE`
|
||||
- `name TEXT NOT NULL`
|
||||
- `description TEXT`
|
||||
|
||||
초기 권장 역할:
|
||||
- `super_admin`
|
||||
- `org_admin`
|
||||
- `viewer`
|
||||
|
||||
### `auth.user_roles`
|
||||
|
||||
사용자와 역할의 다대다 연결.
|
||||
|
||||
주요 컬럼:
|
||||
- `user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE`
|
||||
- `role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE`
|
||||
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
||||
- `PRIMARY KEY (user_id, role_id)`
|
||||
|
||||
### `auth.permissions`
|
||||
|
||||
세분화된 권한 코드 정의.
|
||||
|
||||
주요 컬럼:
|
||||
- `id BIGSERIAL PRIMARY KEY`
|
||||
- `code TEXT NOT NULL UNIQUE`
|
||||
- `name TEXT NOT NULL`
|
||||
- `description TEXT`
|
||||
|
||||
초기 권장 권한:
|
||||
- `member.read`
|
||||
- `member.write`
|
||||
- `member.import`
|
||||
- `seatmap.read`
|
||||
- `seatmap.write`
|
||||
- `photo.upload`
|
||||
- `admin.user.manage`
|
||||
|
||||
### `auth.role_permissions`
|
||||
|
||||
역할과 권한의 다대다 연결.
|
||||
|
||||
주요 컬럼:
|
||||
- `role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE`
|
||||
- `permission_id BIGINT NOT NULL REFERENCES auth.permissions(id) ON DELETE CASCADE`
|
||||
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
||||
- `PRIMARY KEY (role_id, permission_id)`
|
||||
|
||||
### `auth.sessions`
|
||||
|
||||
서버 세션 저장 테이블.
|
||||
|
||||
주요 컬럼:
|
||||
- `id UUID PRIMARY KEY`
|
||||
- `user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE`
|
||||
- `refresh_token_hash TEXT`
|
||||
- `issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
||||
- `expires_at TIMESTAMPTZ NOT NULL`
|
||||
- `revoked_at TIMESTAMPTZ`
|
||||
- `ip_address INET`
|
||||
- `user_agent TEXT`
|
||||
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
||||
|
||||
원칙:
|
||||
- 브라우저 쿠키에는 세션 식별자만 저장
|
||||
- 토큰 자체를 평문으로 DB에 저장하지 않음
|
||||
- 만료와 강제 로그아웃을 DB에서 통제 가능하게 함
|
||||
|
||||
### `auth.login_audit_logs`
|
||||
|
||||
로그인 시도와 결과 기록.
|
||||
|
||||
주요 컬럼:
|
||||
- `id BIGSERIAL PRIMARY KEY`
|
||||
- `username TEXT NOT NULL`
|
||||
- `user_id BIGINT NULL REFERENCES auth.users(id) ON DELETE SET NULL`
|
||||
- `success BOOLEAN NOT NULL`
|
||||
- `failure_reason TEXT`
|
||||
- `ip_address INET`
|
||||
- `user_agent TEXT`
|
||||
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
|
||||
|
||||
용도:
|
||||
- 로그인 실패 추적
|
||||
- 계정 잠금 기준 판단
|
||||
- 보안 감사 대응
|
||||
|
||||
## Relationship To Current `members`
|
||||
|
||||
핵심은 `auth.users.member_id -> public.members.id` 연결이다.
|
||||
|
||||
의미:
|
||||
- 로그인 계정과 조직도 인원을 분리한다
|
||||
- 로그인하지 않는 구성원은 `members` 에만 있어도 된다
|
||||
- 외부 관리자 계정은 `member_id` 없이 운영할 수 있다
|
||||
|
||||
권장 규칙:
|
||||
- 일반 사내 사용자는 `employee_id` 기준으로 계정-구성원 연결
|
||||
- 엑셀 동기화 시 `members.id` 유지가 중요하므로 이미 반영한 비교 기반 동기화 방식을 유지
|
||||
- `member_id` 연결이 끊긴 계정은 자동 삭제하지 말고 관리자 검토 대상으로 둔다
|
||||
|
||||
## Login Flow
|
||||
|
||||
### 1. 로그인 요청
|
||||
|
||||
입력:
|
||||
- `username`
|
||||
- `password`
|
||||
|
||||
처리:
|
||||
- `auth.users` 에서 `username` 조회
|
||||
- `status != active` 이면 거부
|
||||
- `password_hash` 검증
|
||||
- 성공 시 `auth.sessions` 생성
|
||||
- `auth.login_audit_logs` 기록
|
||||
|
||||
응답 권장:
|
||||
- 사용자 기본 정보
|
||||
- 역할 목록
|
||||
- 권한 목록
|
||||
- 세션 만료 시각
|
||||
|
||||
### 2. 인증 확인
|
||||
|
||||
각 보호 API 요청 시:
|
||||
- 세션 쿠키 또는 Bearer 토큰 확인
|
||||
- `auth.sessions` 조회
|
||||
- 만료/폐기 여부 확인
|
||||
- 사용자 상태와 역할 재검증
|
||||
|
||||
### 3. 로그아웃
|
||||
|
||||
처리:
|
||||
- 현재 세션의 `revoked_at` 업데이트
|
||||
- 클라이언트 쿠키 제거
|
||||
|
||||
## Authorization Model
|
||||
|
||||
초기에는 RBAC 기반으로 충분하다.
|
||||
|
||||
권장 역할별 범위:
|
||||
|
||||
`super_admin`
|
||||
- 사용자 관리
|
||||
- 권한 관리
|
||||
- 조직도/사진/자리배치 전체 수정
|
||||
|
||||
`org_admin`
|
||||
- 조직도 조회/수정
|
||||
- 엑셀 임포트
|
||||
- 사진 업로드
|
||||
- 자리배치 수정
|
||||
|
||||
`viewer`
|
||||
- 조직도 조회
|
||||
- 자리배치 조회
|
||||
|
||||
API 보호 예시:
|
||||
- `GET /api/members`: `member.read`
|
||||
- `POST /api/members/import`: `member.import`
|
||||
- `POST /api/uploads/profile-photo`: `photo.upload`
|
||||
- `PUT /api/seat-maps/{seat_map_id}/layout`: `seatmap.write`
|
||||
- 사용자 관리 API: `admin.user.manage`
|
||||
|
||||
## Password Policy
|
||||
|
||||
비밀번호는 평문 저장 금지.
|
||||
|
||||
권장:
|
||||
- `Argon2id` 우선
|
||||
- 대안으로 `bcrypt`
|
||||
|
||||
추가 원칙:
|
||||
- 첫 구현부터 해시 알고리즘 버전 정보 포함
|
||||
- 비밀번호 변경 시 `password_changed_at` 갱신
|
||||
- 실패 횟수 기반 잠금은 앱 로직 또는 별도 컬럼으로 확장 가능
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1
|
||||
|
||||
인증 스키마와 기본 테이블만 추가.
|
||||
|
||||
작업:
|
||||
- `CREATE SCHEMA IF NOT EXISTS auth`
|
||||
- `auth.users`
|
||||
- `auth.roles`
|
||||
- `auth.user_roles`
|
||||
- `auth.permissions`
|
||||
- `auth.role_permissions`
|
||||
- `auth.sessions`
|
||||
- `auth.login_audit_logs`
|
||||
|
||||
이 단계에서는 기존 `/api/mock-login` 유지 가능.
|
||||
|
||||
### Phase 2
|
||||
|
||||
관리자 1명 이상을 수동 생성하고 실제 로그인 API 추가.
|
||||
|
||||
권장 추가 API:
|
||||
- `POST /api/auth/login`
|
||||
- `POST /api/auth/logout`
|
||||
- `GET /api/auth/me`
|
||||
|
||||
### Phase 3
|
||||
|
||||
기존 프론트엔드의 mock 로그인 제거.
|
||||
|
||||
변경 대상:
|
||||
- [frontend/public/app.js](/home/hyunho/projects/mh-dashboard-organization/frontend/public/app.js)
|
||||
- [backend/app/main.py](/home/hyunho/projects/mh-dashboard-organization/backend/app/main.py)
|
||||
- [backend/app/config.py](/home/hyunho/projects/mh-dashboard-organization/backend/app/config.py)
|
||||
|
||||
### Phase 4
|
||||
|
||||
권한 기반으로 API 보호 적용.
|
||||
|
||||
우선순위:
|
||||
1. 쓰기 API 보호
|
||||
2. 업로드 API 보호
|
||||
3. 읽기 API 권한 정리
|
||||
|
||||
## Recommended SQL Skeleton
|
||||
|
||||
```sql
|
||||
CREATE SCHEMA IF NOT EXISTS auth;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULL,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
password_changed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.user_roles (
|
||||
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth.sessions (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
refresh_token_hash TEXT,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- 엑셀 임포트는 계속 `public.members` 기준으로 처리
|
||||
- 로그인 계정 생성은 엑셀 업로드와 분리
|
||||
- 프로필 사진은 현재처럼 파일 저장 + `members.photo_url` 참조 유지 가능
|
||||
- 감사로그와 세션은 삭제보다 보존 기간 정책으로 관리
|
||||
|
||||
## Decision
|
||||
|
||||
현재 프로젝트의 권장안은 아래 한 줄로 정리된다.
|
||||
|
||||
"로그인 DB는 `auth` 스키마로 분리하고, 업무 DB는 `public` 에 유지하며, 두 영역은 `auth.users.member_id` 로만 연결한다."
|
||||
237
docs/DEVELOPMENT_HISTORY.md
Normal file
237
docs/DEVELOPMENT_HISTORY.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Development History
|
||||
|
||||
## Purpose
|
||||
|
||||
이 문서는 `total` 브랜치에서 진행한 통합 작업을 기능 단위로 정리한 개발 히스토리다.
|
||||
목표는 다음 두 가지다.
|
||||
|
||||
- 지금까지 어떤 기능을 어떤 방식으로 붙였는지 빠르게 파악
|
||||
- 이후 유지보수나 추가 개발 시, 왜 그렇게 구현했는지 추적 가능하게 하기
|
||||
|
||||
## 1. 대시보드 허브 통합
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- `조직 현황`, `프로젝트별 분석`, `팀/개인별 분석`, `자리배치도`를 하나의 메인 허브에서 오갈 수 있도록 통합
|
||||
- 공통 헤더, 로그인 정보, 탭 전환, 공통 기간 캘린더 구성
|
||||
- `payment.html`, `mh.html`은 초기에는 iframe 연결 방식으로 편입
|
||||
|
||||
### 해결 방식
|
||||
|
||||
- 기존 화면을 새로 재개발하지 않고, 먼저 메인 허브 안에 편입
|
||||
- 이후 데이터는 공통 API/DB 기준으로 전환하는 2단계 방식 채택
|
||||
|
||||
## 2. 조직현황 고도화
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- 조직도 레거시 화면을 메인 허브에 연결
|
||||
- 관리자/비관리자 모드 정리
|
||||
- 구성원 상세 프로필 개선
|
||||
- 프로필 사진 업로드 연동
|
||||
- `재석위치` 카드 추가
|
||||
|
||||
### 해결 방식
|
||||
|
||||
- 레거시 조직도 화면은 유지하되 API를 통해 `members`를 읽는 구조로 전환
|
||||
- 상세 프로필의 `재석위치`는 문자열이 아니라 실제 자리배치도 viewer preview를 붙이는 방식으로 변경
|
||||
|
||||
## 3. 자리배치도 기능 재구성
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- `기술개발센터` 고정 도면 기준 자리배치도 viewer 구성
|
||||
- 관리자 편집 화면과 비관리자 열람 화면 분리
|
||||
- 조직도에서 `+` 버튼으로 자리배치도 진입
|
||||
- 배치된 좌석의 이름/직급 라벨 표시
|
||||
- 좌석 hover, 클릭, 자리 비우기, 미배치 목록 복귀 흐름 구현
|
||||
|
||||
### 해결 방식
|
||||
|
||||
- DXF 업로드 기반 편집을 그대로 밀기보다, 실제 업무에 맞는 고정 도면 중심 구조로 정리
|
||||
- viewer 코드는 공통으로 쓰되, 화면은
|
||||
- 관리자 편집
|
||||
- 비관리자 열람
|
||||
- 구성원 상세 preview
|
||||
로 역할 분리
|
||||
|
||||
## 4. 자리배치도 저장 문제 해결
|
||||
|
||||
### 문제
|
||||
|
||||
- 관리자 화면에서는 배치가 된 것처럼 보여도 저장 후 조직도 상세 프로필에서는 `미배치`
|
||||
- 비관리자 자리배치도에서도 배치 결과가 보이지 않음
|
||||
|
||||
### 원인
|
||||
|
||||
- `seat_positions_map_cell_idx (seat_map_id, row_index, col_index)` 유니크 인덱스가 슬롯 기반 도면에도 그대로 적용됨
|
||||
- 고정 도면 배치는 실제로 `seat_slot_id` 기준 저장인데, 모든 배치가 `(row_index=0, col_index=0)`으로 들어가 충돌
|
||||
- 그 결과 두 번째 좌석부터 `500 Internal Server Error`로 저장 실패
|
||||
|
||||
### 해결
|
||||
|
||||
- 슬롯 기반 도면에서는 `(seat_map_id, row_index, col_index)` 인덱스가 적용되지 않도록 수정
|
||||
- `seat_positions` 저장 후 `members.seat_label`도 같이 동기화
|
||||
- 조직도 iframe은 저장 완료 후 `seatmap-layout-updated` 메시지를 받아 cache를 비우고 재조회
|
||||
|
||||
### 결과
|
||||
|
||||
- 관리자 자리배치 저장이 실제 DB까지 반영됨
|
||||
- 저장된 배치는 재접속 후에도 유지
|
||||
- 조직현황 상세 프로필과 비관리자 자리배치도에서도 같은 배치를 읽을 수 있는 구조가 됨
|
||||
|
||||
## 5. 통합 DB 구축
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- `organization.xlsx`
|
||||
- `MH.xlsx`
|
||||
- `payment.csv`
|
||||
|
||||
세 원본을 하나의 PostgreSQL 기준으로 적재
|
||||
|
||||
### 핵심 테이블
|
||||
|
||||
- `members`
|
||||
- `seat_maps`
|
||||
- `seat_slots`
|
||||
- `seat_positions`
|
||||
- `integration_raw_organization_rows`
|
||||
- `integration_raw_mh_rows`
|
||||
- `integration_raw_payment_rows`
|
||||
- `integration_work_logs`
|
||||
- `integration_work_log_segments`
|
||||
- `integration_vouchers`
|
||||
- `integration_projects`
|
||||
- `integration_project_category_mappings`
|
||||
|
||||
### 해결 방식
|
||||
|
||||
- 원본 보존용 raw 테이블과 운영용 표준 테이블을 분리
|
||||
- 화면이 직접 파일을 읽지 않고, DB와 API를 통해 같은 데이터를 보도록 정리
|
||||
|
||||
## 6. 프로젝트별 분석 통합
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- `opayment.html` 원본 기준으로 화면, 카테고리, 계산식 복원
|
||||
- `payment.csv`와 `MH.xlsx`를 함께 쓰는 구조로 정리
|
||||
- `payment.csv` 분류를 1순위, `ptj.csv` 프로젝트 매핑을 2순위로 적용
|
||||
|
||||
### 해결한 문제
|
||||
|
||||
- 연장근무 시간을 `실제`가 아니라 `가공` 기준으로 써야 원본과 총합이 맞음
|
||||
- 프로젝트 분류는 `payment.csv`만으로 부족해 `ptj.csv` 보정이 필요
|
||||
|
||||
### 결과
|
||||
|
||||
- 총합과 대부분의 프로젝트 집계가 원본 `opayment` 기준에 가깝게 정렬됨
|
||||
- 남은 차이는 소수점 또는 추가 매핑 보정 수준으로 축소
|
||||
|
||||
## 7. 팀/개인별 분석 통합
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- `omh.html` 원본 기준으로 화면, 카테고리, 계산식 복원
|
||||
- 업로드형이 아니라 통합 DB raw MH 데이터를 다시 공급하는 방식으로 전환
|
||||
|
||||
### 해결 방식
|
||||
|
||||
- 원본 `MH.xlsx` 시트 구조가 깨지지 않도록 row 배열 자체를 DB에 저장
|
||||
- 팀별 진행 프로젝트 등 원본 계산식이 기대하는 입력 구조를 그대로 재현
|
||||
|
||||
## 8. 조직 데이터 운영 정리
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- 이름 alias, 퇴사 제외, 조직 override를 코드 상수에서 제거
|
||||
- DB 테이블 기반 운영으로 전환
|
||||
|
||||
### 운영 테이블
|
||||
|
||||
- `member_aliases`
|
||||
- `member_retirements`
|
||||
- `member_overrides`
|
||||
|
||||
### 결과
|
||||
|
||||
- 이름 치환, 퇴사 제외, 팀/직책 예외를 코드 수정 없이 DB에서 관리 가능
|
||||
|
||||
## 9. 외부 접속 설정
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- WSL 내부 `0.0.0.0:8080` 바인딩 확인
|
||||
- Windows host에서 `portproxy`와 방화벽 규칙으로 다른 PC 접속 가능하게 정리
|
||||
|
||||
### 유의사항
|
||||
|
||||
- Windows LAN IP 또는 WSL IP가 바뀌면 `portproxy`의 `connectaddress`는 다시 맞춰야 한다
|
||||
- 운영 안정성을 위해 향후 자동화 스크립트화가 필요함
|
||||
|
||||
## 10. 인증 기본 구조 추가
|
||||
|
||||
### 작업 내용
|
||||
|
||||
- 프런트 로그인 화면을 실제 `/api/auth/login` API와 연결
|
||||
- 로그인 세션 확인용 `/api/auth/me` 추가
|
||||
- 로그아웃용 `/api/auth/logout` 추가
|
||||
- 로그인 감사로그와 세션 저장 테이블 추가
|
||||
|
||||
### 해결 방식
|
||||
|
||||
- 업무 데이터는 기존 `members` 중심으로 유지
|
||||
- 인증 데이터는 `auth.users`, `auth.sessions`, `auth.login_audit_logs` 로 분리
|
||||
- 구성원 import 시 사번 기준으로 계정을 동기화하고 기본 관리자 계정을 seed
|
||||
|
||||
### 현재 한계
|
||||
|
||||
- 권한 모델은 아직 `role` 단일 컬럼 수준이다
|
||||
- API별 세부 권한 검증은 아직 미완성이다
|
||||
- `/api/mock-login` 은 아직 남아 있어 운영 기준으로는 정리가 필요하다
|
||||
|
||||
## 11. 이력형 DB 전환 방향 확정
|
||||
|
||||
### 배경
|
||||
|
||||
- 월간 스냅샷 파일보다, 사용자가 원하는 날짜 기준으로 조직도와 자리배치도를 바로 조회하는 요구가 더 중요해졌다
|
||||
- 조직도 기본 정보나 자리배치 정보처럼 원래 날짜가 없는 데이터도 과거/현재 버전 차이를 추적해야 한다
|
||||
|
||||
### 결정
|
||||
|
||||
- 월간 스냅샷 기능은 범위에서 제외
|
||||
- 대신 DB 자체를 `valid_from`, `valid_to` 기반 버전 구조로 전환
|
||||
- 사용자 조회는 파일 스냅샷이 아니라 `as_of` 기준 조회 방식으로 설계
|
||||
|
||||
### 우선 적용 대상
|
||||
|
||||
- `members` -> `member_versions`
|
||||
- `seat_positions` -> `seat_assignment_versions`
|
||||
|
||||
### 기대 효과
|
||||
|
||||
- 특정 날짜의 조직 상태 재구성 가능
|
||||
- 특정 날짜의 자리배치도 재구성 가능
|
||||
- 기간 비교나 변경 추적 UI로 확장 가능
|
||||
|
||||
### 현재 반영 상태
|
||||
|
||||
- `history_revisions`
|
||||
- `member_versions`
|
||||
- `seat_assignment_versions`
|
||||
- `entity_change_events`
|
||||
|
||||
초기 단계로 테이블과 baseline backfill 경로를 먼저 추가했다.
|
||||
아직 조직도/자리배치도 쓰기 API가 매 수정마다 version row 를 append 하도록 완전히 전환된 상태는 아니다.
|
||||
|
||||
### 설계 문서
|
||||
|
||||
- [HISTORY_ASOF_DB_PLAN.md](/home/hyunho/projects/mh-dashboard-organization/docs/HISTORY_ASOF_DB_PLAN.md)
|
||||
|
||||
## Next Focus
|
||||
|
||||
- `#2` 영속성 운영 검증과 문서 기준 정리
|
||||
- 권한 제어와 mock login 정리
|
||||
- `#9` as-of date 기반 history 구조 설계 및 점진적 도입
|
||||
- 자리배치도 조직 트리, 나머지 사무실 도면 등 실사용 기능 고도화
|
||||
- 프로젝트별 분석의 남은 소수점/분류 오차 정리
|
||||
216
docs/DEV_PROD_DB_PROTOCOL.md
Normal file
216
docs/DEV_PROD_DB_PROTOCOL.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Dev / Prod DB Protocol
|
||||
|
||||
## 목적
|
||||
|
||||
- `8081` 작업용은 기능 개발과 화면 검증을 먼저 수행하는 환경이다.
|
||||
- `8080` 공개용은 실제 기준 데이터와 운영 화면을 제공하는 환경이다.
|
||||
- 코드와 데이터의 기준을 분리해서 관리하되, 데이터 정본은 항상 `8080` 공개용 DB로 유지한다.
|
||||
|
||||
## 현재 구조
|
||||
|
||||
### 코드 경로
|
||||
|
||||
- 공개용 `8080`: `/home/hyunho/projects/mh-dashboard-organization`
|
||||
- 작업용 `8081`: `/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081`
|
||||
|
||||
### 작업용 Compose 기준
|
||||
|
||||
- 공개용 `8080` stack: `docker-compose.yml`
|
||||
- 작업용 `8081` stack: `docker-compose.8081.yml`
|
||||
- 작업용 project name 기본값: `mh-dashboard-organization-dev`
|
||||
- 작업용 `8081`는 반드시 `/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081`에서 띄운다
|
||||
|
||||
### DB 볼륨
|
||||
|
||||
- 공개용 `8080`: `mh-dashboard-organization_postgres_data`
|
||||
- 작업용 `8081`: `mh-dashboard-organization-dev_postgres_data`
|
||||
|
||||
즉 현재는 `8080` 과 `8081` 이 코드 workspace 와 DB volume 모두 분리된 상태로 운영한다.
|
||||
|
||||
## 정본 기준
|
||||
|
||||
- 코드 선행 환경: `8081`
|
||||
- 데이터 정본: `8080`
|
||||
- 공개 반영 기준: `8081`에서 검증 완료된 코드만 `8080`에 승격
|
||||
|
||||
중요:
|
||||
- `8081` DB는 독립 정본이 아니다.
|
||||
- `8081` DB는 `8080` DB를 기준으로 맞춘 검증용 복제본이어야 한다.
|
||||
|
||||
## 왜 이 규칙이 필요한가
|
||||
|
||||
- 조직현황, 조직도, 자리배치 인원, 퇴사자 제외, 멤버 수는 코드보다 DB 영향이 크다.
|
||||
- 작업용 DB가 공개용과 달라지면 기능 검증 결과 자체가 왜곡된다.
|
||||
- 원인 분석 시 `코드 차이`와 `DB 차이`를 분리할 수 있어야 한다.
|
||||
|
||||
## 현재 확인된 차이 예시
|
||||
|
||||
2026-03-27 확인 기준:
|
||||
|
||||
- `members`
|
||||
- `8080`: `227`
|
||||
- `8081`: `236`
|
||||
- `member_retirements`
|
||||
- `8080`: `9`
|
||||
- `8081`: `0`
|
||||
- `seat_maps`
|
||||
- `8080`: `21`
|
||||
- `8081`: `3`
|
||||
- `seat_positions`
|
||||
- `8080`: `5`
|
||||
- `8081`: `0`
|
||||
- `seat_slots`
|
||||
- `8080`: `57308`
|
||||
- `8081`: `370`
|
||||
|
||||
## 기준 테이블 분류
|
||||
|
||||
### A. 공개용 정본 기준으로 항상 맞춰야 하는 테이블
|
||||
|
||||
- `members`
|
||||
- `member_aliases`
|
||||
- `member_overrides`
|
||||
- `member_retirements`
|
||||
- `seat_maps`
|
||||
- `seat_slots`
|
||||
- `seat_positions`
|
||||
|
||||
### B. 원본 재적재로 다시 만들 수 있는 통합 테이블
|
||||
|
||||
- `integration_import_batches`
|
||||
- `integration_raw_organization_rows`
|
||||
- `integration_raw_mh_rows`
|
||||
- `integration_raw_mh_pm_rows`
|
||||
- `integration_raw_payment_rows`
|
||||
- `integration_projects`
|
||||
- `integration_project_aliases`
|
||||
- `integration_project_category_mappings`
|
||||
- `integration_project_pm_assignments`
|
||||
- `integration_work_logs`
|
||||
- `integration_work_log_segments`
|
||||
- `integration_vouchers`
|
||||
|
||||
### C. 별도 정책이 필요한 영역
|
||||
|
||||
- `snapshots`
|
||||
- 인증 관련 스키마와 테이블
|
||||
|
||||
## 작업 프로토콜
|
||||
|
||||
### 1. 작업 시작 전
|
||||
|
||||
1. `8080`과 `8081` 모두 기동 상태 확인
|
||||
2. 이번 작업이 `코드 변경`인지 `데이터 변경`인지 먼저 구분
|
||||
3. 공개용 기준 데이터가 필요한 화면이면 `8081` DB를 먼저 `8080` 기준으로 맞춤
|
||||
4. 작업 전후 검증은 [REGRESSION_CHECKLIST.md](/home/hyunho/projects/mh-dashboard-organization/docs/REGRESSION_CHECKLIST.md) 기준으로 수행
|
||||
|
||||
### 2. 기능 개발 중
|
||||
|
||||
1. 코드 수정은 먼저 `8081`에서 수행
|
||||
2. UI, 계산식, 자리배치도 동작은 `8081`에서 확인
|
||||
3. 조직도/멤버/자리배치 검증은 공개용 기준 데이터가 반영된 `8081` DB에서만 수행
|
||||
|
||||
### 3. 검증 완료 후
|
||||
|
||||
1. 코드만 `8080`으로 승격
|
||||
2. 데이터 반영이 필요한 기능은 별도 절차를 문서화한 뒤 적용
|
||||
3. 공개용 DB를 개발 실험용으로 사용하지 않음
|
||||
|
||||
## 금지 사항
|
||||
|
||||
- `8081` DB를 장기간 독립 정본처럼 취급하지 않기
|
||||
- 퇴사자, 멤버, 좌석 정보를 작업용에서 수작업으로만 유지하지 않기
|
||||
- DB 차이를 무시하고 `8081` 검증 결과가 `8080`과 같다고 가정하지 않기
|
||||
|
||||
## 권장 동기화 범위
|
||||
|
||||
### 최소 범위
|
||||
|
||||
조직도/자리배치도 검증 전 반드시 동기화:
|
||||
|
||||
1. `members`
|
||||
2. `member_aliases`
|
||||
3. `member_overrides`
|
||||
4. `member_retirements`
|
||||
5. `seat_maps`
|
||||
6. `seat_slots`
|
||||
7. `seat_positions`
|
||||
|
||||
### 전체 범위
|
||||
|
||||
분석 화면까지 공개용 기준으로 검증해야 하면 아래도 포함:
|
||||
|
||||
1. `integration_import_batches`
|
||||
2. `integration_raw_*`
|
||||
3. `integration_projects`
|
||||
4. `integration_project_*`
|
||||
5. `integration_work_logs`
|
||||
6. `integration_work_log_segments`
|
||||
7. `integration_vouchers`
|
||||
|
||||
## 세션 시작 체크
|
||||
|
||||
1. 지금 작업이 `코드 변경`인지 `데이터 변경`인지 구분
|
||||
2. 공개용 기준 데이터가 필요한지 판단
|
||||
3. 필요하면 `8081` DB를 `8080` 기준으로 먼저 동기화
|
||||
4. 그 뒤 기능 개발과 검증 수행
|
||||
5. 검증은 [REGRESSION_CHECKLIST.md](/home/hyunho/projects/mh-dashboard-organization/docs/REGRESSION_CHECKLIST.md) 기준으로 수행
|
||||
6. 검증 완료 후 공개용에 코드 승격
|
||||
|
||||
## 다음 액션
|
||||
|
||||
- `8081` DB를 `8080` 기준으로 맞추는 반복 가능한 동기화 절차를 만든다
|
||||
- 최소한 `A 그룹` 테이블은 수동 기억에 의존하지 않고 다시 수행 가능해야 한다
|
||||
- 이후 모든 작업은 이 문서를 기본 프로토콜로 따른다
|
||||
|
||||
## 실행 절차
|
||||
|
||||
반복 가능한 동기화 스크립트:
|
||||
|
||||
- [sync_prod_db_to_dev.sh](/home/hyunho/projects/mh-dashboard-organization/scripts/sync_prod_db_to_dev.sh)
|
||||
- [docker-compose.8081.yml](/home/hyunho/projects/mh-dashboard-organization/docker-compose.8081.yml)
|
||||
|
||||
사용 방법:
|
||||
|
||||
```bash
|
||||
./scripts/prepare_dev_worktree.sh
|
||||
cd /home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081
|
||||
docker compose -p mh-dashboard-organization-dev --env-file .env -f docker-compose.8081.yml up -d --build
|
||||
./scripts/sync_prod_db_to_dev.sh minimal
|
||||
./scripts/sync_prod_db_to_dev.sh full
|
||||
```
|
||||
|
||||
`prepare_dev_worktree.sh`가 같이 처리하는 것:
|
||||
|
||||
- 메인 workspace를 `.dev-worktree-8081`로 복제 또는 재사용
|
||||
- `.env` 복사
|
||||
- 로컬 전용 디자인 참고 자산 복사
|
||||
- `incoming-files/sample style.css`
|
||||
- `incoming-files/260320.html`
|
||||
- `incoming-files/사업관리대장/`
|
||||
- `incoming-files/1.png`
|
||||
- `incoming-files/seat/center_chair_people_map(2).html`
|
||||
|
||||
중요:
|
||||
|
||||
- `8081`은 현재 메인 workspace를 직접 마운트하면 안 된다
|
||||
- 컨테이너가 `/home/hyunho/projects/mh-dashboard-organization/...`를 물고 있으면 분리 상태가 깨진 것이다
|
||||
- 정상 상태는 `docker inspect mh-dashboard-organization-dev-backend-1` 기준 마운트 소스가 `/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/...`로 나와야 한다
|
||||
|
||||
규칙:
|
||||
|
||||
- `minimal`
|
||||
- 조직도, 멤버, 자리배치도 검증 전 사용
|
||||
- `full`
|
||||
- 분석 화면까지 공개용 기준 데이터로 맞춰야 할 때 사용
|
||||
|
||||
주의:
|
||||
|
||||
- 스크립트는 동기화 전에 `8081`의 `proxy`, `frontend`, `backend` 를 잠시 멈춘다
|
||||
- 이유는 중간 상태를 읽는 API 요청과 DB truncate/restore 가 충돌하면 deadlock 또는 부분 검증이 발생할 수 있기 때문이다
|
||||
- 스크립트는 `8080` DB 데이터를 덤프해서 `8081` DB의 대상 테이블을 비우고 다시 적재한다
|
||||
- `8081`에서만 존재하던 대상 테이블 데이터는 사라진다
|
||||
- `seat_positions` 는 portable CSV 경로로 별도 복원한다
|
||||
- 복원 후 `members.seat_label`, `auth.users`, history backfill 을 다시 맞춘다
|
||||
- 실행 후 주요 테이블 수량과 seat 정합성 수치를 출력한다
|
||||
- 따라서 실행 전 현재 작업용 DB 상태를 유지해야 하면 별도 백업 후 실행한다
|
||||
294
docs/HISTORY_ASOF_DB_PLAN.md
Normal file
294
docs/HISTORY_ASOF_DB_PLAN.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# History / As-Of DB Plan
|
||||
|
||||
## Goal
|
||||
|
||||
월간 스냅샷 파일을 따로 만드는 대신, DB 자체를 시간축이 있는 구조로 전환한다.
|
||||
목표는 다음과 같다.
|
||||
|
||||
- 조직도와 자리배치도를 수정할 때마다 과거 값이 사라지지 않게 누적 저장
|
||||
- 사용자가 특정 날짜 또는 기간을 선택하면 그 시점 기준 상태를 다시 조회
|
||||
- 날짜가 원래 없는 데이터도 `유효 시작일`과 `유효 종료일`을 부여해 과거 버전 조회 가능하게 만들기
|
||||
|
||||
핵심 원칙은 아래 한 줄이다.
|
||||
|
||||
- 최신 값을 덮어쓰지 않고, `valid_from`, `valid_to` 기반 버전 행을 누적한다
|
||||
|
||||
## Why This Instead Of Snapshots
|
||||
|
||||
- 월간 스냅샷 파일은 생성 시점만 남고 중간 변경 추적이 약하다
|
||||
- 원하는 날짜 기준으로 바로 조회하기 어렵다
|
||||
- 조직도만 따로 파일로 남으면 자리배치도, 권한, 운영 이력을 함께 맞추기 어렵다
|
||||
|
||||
따라서 이 프로젝트에는 "파일 스냅샷"보다 "시점 조회 가능한 버전 DB"가 더 맞다.
|
||||
|
||||
## Query Model
|
||||
|
||||
조회 기준은 `as_of` 또는 `date_from`, `date_to` 이다.
|
||||
|
||||
- 특정 날짜 조회:
|
||||
- `GET /api/members?as_of=2026-03-01`
|
||||
- `GET /api/seat-maps/active/layout?as_of=2026-03-01`
|
||||
- 기간 비교:
|
||||
- `GET /api/history/organization/compare?date_from=2026-03-01&date_to=2026-03-31`
|
||||
|
||||
공통 조회 조건은 아래다.
|
||||
|
||||
```sql
|
||||
WHERE valid_from <= :as_of
|
||||
AND (valid_to IS NULL OR valid_to > :as_of)
|
||||
```
|
||||
|
||||
## Recommended Data Model
|
||||
|
||||
### 1. Stable Base Tables
|
||||
|
||||
식별자와 최소 메타만 유지하는 기준 테이블.
|
||||
|
||||
```sql
|
||||
CREATE TABLE members (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE TABLE seat_assignment_targets (
|
||||
member_id INTEGER PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
설명:
|
||||
- `members` 는 "사람 자체" 식별자 역할
|
||||
- 실제 이름, 조직, 직급, 연락처, 좌석 같은 표시 데이터는 버전 테이블로 이동
|
||||
|
||||
### 2. Member Version Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE member_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
company TEXT NOT NULL DEFAULT '',
|
||||
rank TEXT NOT NULL DEFAULT '',
|
||||
role TEXT NOT NULL DEFAULT '',
|
||||
department TEXT NOT NULL DEFAULT '',
|
||||
grp TEXT NOT NULL DEFAULT '',
|
||||
division TEXT NOT NULL DEFAULT '',
|
||||
team TEXT NOT NULL DEFAULT '',
|
||||
cell TEXT NOT NULL DEFAULT '',
|
||||
work_status TEXT NOT NULL DEFAULT '',
|
||||
work_time TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
photo_url TEXT NOT NULL DEFAULT '',
|
||||
valid_from TIMESTAMPTZ NOT NULL,
|
||||
valid_to TIMESTAMPTZ,
|
||||
revision_no BIGINT NOT NULL,
|
||||
changed_by_user_id BIGINT,
|
||||
change_reason TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX member_versions_member_time_idx
|
||||
ON member_versions (member_id, valid_from, valid_to);
|
||||
```
|
||||
|
||||
설명:
|
||||
- 날짜가 원래 없던 조직도 데이터도 이 테이블에서 과거 버전 관리
|
||||
- 어떤 시점에 이름, 조직, 직책, 연락처가 어땠는지 재구성 가능
|
||||
|
||||
### 3. Seat Assignment Version Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE seat_assignment_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
||||
seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE,
|
||||
seat_slot_id INTEGER REFERENCES seat_slots(id) ON DELETE CASCADE,
|
||||
seat_label TEXT NOT NULL DEFAULT '',
|
||||
valid_from TIMESTAMPTZ NOT NULL,
|
||||
valid_to TIMESTAMPTZ,
|
||||
revision_no BIGINT NOT NULL,
|
||||
changed_by_user_id BIGINT,
|
||||
change_reason TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX seat_assignment_versions_member_time_idx
|
||||
ON seat_assignment_versions (member_id, valid_from, valid_to);
|
||||
```
|
||||
|
||||
설명:
|
||||
- 현재 `seat_positions` 가 맡는 "최신 좌석 상태"를 버전형으로 저장
|
||||
- 특정 날짜의 자리배치도를 다시 그릴 수 있음
|
||||
|
||||
### 4. Optional Change Event Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE entity_change_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
revision_no BIGINT NOT NULL,
|
||||
changed_by_user_id BIGINT,
|
||||
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
change_reason TEXT NOT NULL DEFAULT '',
|
||||
patch_json JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
```
|
||||
|
||||
설명:
|
||||
- 버전 테이블은 "그 시점의 전체 값"
|
||||
- 이벤트 테이블은 "무엇이 바뀌었는지"
|
||||
- 초기에는 없어도 되지만, 추후 비교 UI와 감사로그에 유용
|
||||
|
||||
### 5. Revision Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE history_revisions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scope TEXT NOT NULL DEFAULT 'organization',
|
||||
revision_label TEXT NOT NULL,
|
||||
created_by_user_id BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
note TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
```
|
||||
|
||||
설명:
|
||||
- 버전 묶음을 사람 친화적으로 관리할 때 사용
|
||||
- 예: `2026-03-27 1차 조직개편 반영`
|
||||
|
||||
## How Writes Change
|
||||
|
||||
현재 구조:
|
||||
- `UPDATE members SET ...`
|
||||
- `UPSERT seat_positions ...`
|
||||
|
||||
바꿀 구조:
|
||||
1. 현재 유효한 버전 행을 조회
|
||||
2. 값이 달라지면 기존 행의 `valid_to` 를 닫음
|
||||
3. 새 값을 가진 행을 `valid_from = now()` 로 insert
|
||||
4. 필요하면 최신 캐시 테이블도 함께 갱신
|
||||
|
||||
예시:
|
||||
|
||||
```sql
|
||||
UPDATE member_versions
|
||||
SET valid_to = NOW()
|
||||
WHERE member_id = :member_id
|
||||
AND valid_to IS NULL;
|
||||
|
||||
INSERT INTO member_versions (
|
||||
member_id, name, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, photo_url,
|
||||
valid_from, valid_to, revision_no, changed_by_user_id, change_reason
|
||||
)
|
||||
VALUES (
|
||||
:member_id, :name, :company, :rank, :role, :department, :grp, :division, :team, :cell,
|
||||
:work_status, :work_time, :phone, :email, :photo_url,
|
||||
NOW(), NULL, :revision_no, :changed_by_user_id, :change_reason
|
||||
);
|
||||
```
|
||||
|
||||
## How Date-Bearing And Date-Less Data Coexist
|
||||
|
||||
### 날짜가 원래 있는 데이터
|
||||
|
||||
- `integration_work_logs.work_date`
|
||||
- `integration_vouchers.issue_date`
|
||||
|
||||
이 데이터는 원래 날짜 컬럼이 있으므로 그대로 사용하면 된다.
|
||||
|
||||
### 날짜가 원래 없는 데이터
|
||||
|
||||
- 조직도 인원 기본 정보
|
||||
- 조직 소속
|
||||
- 자리배치 상태
|
||||
- 사진 경로
|
||||
|
||||
이 데이터는 `valid_from`, `valid_to` 를 붙여 시점 조회가 가능하게 만든다.
|
||||
|
||||
즉, "날짜가 없는 데이터"가 아니라 "유효기간을 부여한 버전 데이터"로 바꾸는 것이다.
|
||||
|
||||
## API Direction
|
||||
|
||||
### Common UI Input
|
||||
|
||||
사용자가 실제 HTML에서 고르는 기준은 헤더의 날짜 제어를 공통 입력으로 쓰는 것이 맞다.
|
||||
|
||||
권장안:
|
||||
- 프로젝트/팀 분석: 기존처럼 `시작일 ~ 종료일`
|
||||
- 조직도/자리배치도: 우선 `기준일(as_of)` 1개를 사용
|
||||
- 필요하면 조직도 비교 화면에서 `비교 시작일`, `비교 종료일` 확장
|
||||
|
||||
현재 상태:
|
||||
- 헤더 날짜 제어는 `프로젝트별 분석`, `팀/개인별 분석` iframe에 이미 전달되고 있음
|
||||
- 조직도/자리배치도는 아직 헤더 날짜를 실제 조회 조건으로 사용하지 않음
|
||||
|
||||
권장 API:
|
||||
|
||||
```text
|
||||
GET /api/members?as_of=2026-03-27
|
||||
GET /api/members/{id}?as_of=2026-03-27
|
||||
GET /api/seat-maps/active/layout?as_of=2026-03-27
|
||||
GET /api/history/organization/compare?date_from=2026-03-01&date_to=2026-03-31
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1. History Tables Add
|
||||
|
||||
- `member_versions`
|
||||
- `seat_assignment_versions`
|
||||
- `history_revisions`
|
||||
- 필요 시 `entity_change_events`
|
||||
|
||||
현재 `members`, `seat_positions` 는 그대로 유지
|
||||
|
||||
### Phase 2. Backfill
|
||||
|
||||
- 현재 `members` 최신값을 `member_versions(valid_from = NOW(), valid_to = NULL)` 로 적재
|
||||
- 현재 `seat_positions` 최신값을 `seat_assignment_versions(valid_from = NOW(), valid_to = NULL)` 로 적재
|
||||
- 이 단계에서는 과거 진짜 이력은 없고 "현재 상태를 버전 구조에 싣는 것"이 목표
|
||||
|
||||
### Phase 3. Dual Write
|
||||
|
||||
- 조직도 수정 시:
|
||||
- 기존 `members` 갱신
|
||||
- 동시에 `member_versions` 에 append
|
||||
- 자리배치 저장 시:
|
||||
- 기존 `seat_positions` 갱신
|
||||
- 동시에 `seat_assignment_versions` 에 append
|
||||
|
||||
### Phase 4. As-Of Read APIs
|
||||
|
||||
- 조직도 API에 `as_of` 지원
|
||||
- 자리배치도 API에 `as_of` 지원
|
||||
- 헤더 날짜 제어와 연결
|
||||
|
||||
### Phase 5. Full History-First Read
|
||||
|
||||
- 최신 조회도 버전 테이블 기준으로 전환
|
||||
- `members`, `seat_positions` 는 캐시 또는 편의 테이블로 축소 가능
|
||||
|
||||
## Recommended First Scope
|
||||
|
||||
처음부터 모든 테이블을 이력화하지 말고 아래부터 시작하는 것이 안전하다.
|
||||
|
||||
1. `members` -> `member_versions`
|
||||
2. `seat_positions` -> `seat_assignment_versions`
|
||||
3. 조직도/자리배치도 조회 API에 `as_of`
|
||||
|
||||
이 세 가지가 되면 사용자는 원하는 날짜의 조직 상태와 좌석 상태를 볼 수 있다.
|
||||
|
||||
## Explicitly Removed From Scope
|
||||
|
||||
- 월간 스냅샷 파일 생성
|
||||
- 스냅샷 다운로드 기능
|
||||
- 조직도만 따로 파일로 내보내는 방식
|
||||
|
||||
이 프로젝트의 방향은 "파일 스냅샷"이 아니라 "시점 조회 가능한 버전 DB"다.
|
||||
@@ -1,5 +1,12 @@
|
||||
# 인프라 검증 체크리스트
|
||||
|
||||
## 현재 확인 상태
|
||||
- 2026-03-27 기준 `docker compose ps` 에서 `proxy`, `frontend`, `backend`, `db` 모두 `healthy`
|
||||
- 2026-03-27 기준 `curl http://localhost:8080/api/health` 정상
|
||||
- 2026-03-27 기준 `curl http://localhost:8080/api/members` 에서 `items` 비어 있지 않음
|
||||
- 다른 PC 접속도 현재 확인됨
|
||||
- 개발/운영 DB 분리 운영 원칙은 [DEV_PROD_DB_PROTOCOL.md](/home/hyunho/projects/mh-dashboard-organization/docs/DEV_PROD_DB_PROTOCOL.md) 기준으로 관리
|
||||
|
||||
## 1. 컨테이너 기동
|
||||
- `docker compose build`
|
||||
- `docker compose up -d`
|
||||
@@ -32,4 +39,8 @@
|
||||
- 확인 기준:
|
||||
- DB 데이터 유지
|
||||
- 업로드 파일 유지
|
||||
- 스냅샷 파일 유지
|
||||
|
||||
## 6. 제외 또는 후속 검증 항목
|
||||
- 월간 스냅샷 파일 유지 검증은 현재 코드 기준 미구현 항목
|
||||
- 스냅샷 기능을 다시 범위에 넣을 경우 별도 API/파일 경로/다운로드 검증 절차를 추가해야 함
|
||||
- `8081`에서 조직도, 멤버, 자리배치도 검증 전에는 `8080` 정본 DB 기준 동기화가 필요함
|
||||
|
||||
263
docs/INTEGRATION_DB_PLAN.md
Normal file
263
docs/INTEGRATION_DB_PLAN.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Integration DB Plan
|
||||
|
||||
## Goal
|
||||
|
||||
`organization.xlsx`, `MH.xlsx`, `payment.csv`를 하나의 통합 DB로 수용하고,
|
||||
`조직 현황`, `프로젝트별 분석`, `팀/개인별 분석`, `자리배치도`가 같은 기준 데이터를 바라보도록 정리한다.
|
||||
|
||||
## Source Summary
|
||||
|
||||
### 1. `organization.xlsx`
|
||||
|
||||
용도:
|
||||
- 인원 기본 정보
|
||||
- 조직 구조
|
||||
|
||||
주요 컬럼:
|
||||
- `name`
|
||||
- `tag`
|
||||
- `rank`
|
||||
- `pos`
|
||||
- `co`
|
||||
- `cell`
|
||||
- `team`
|
||||
- `div`
|
||||
- `gr`
|
||||
- `part`
|
||||
- `ph`
|
||||
- `mail`
|
||||
|
||||
해석:
|
||||
- `tag`는 사번 또는 내부 인원 식별자로 사용
|
||||
- 조직 정보는 `part > gr > div > team > cell` 구조
|
||||
|
||||
### 2. `MH.xlsx`
|
||||
|
||||
용도:
|
||||
- 일자별 인원 근무 실적
|
||||
- 프로젝트별 투입 시간
|
||||
- 연장근무 포함 세부 투입 슬롯
|
||||
|
||||
주요 컬럼:
|
||||
- `근무일자`
|
||||
- `팀 분류`
|
||||
- `팀`
|
||||
- `사원번호`
|
||||
- `이름`
|
||||
- `직책`
|
||||
- `user_state`
|
||||
- `시차시간`
|
||||
- `메인업무/추가업무1~5/연장근무`
|
||||
- `프로젝트 코드`
|
||||
- `프로젝트명`
|
||||
- `서브 코드`
|
||||
- `근무시간`
|
||||
|
||||
추가 시트:
|
||||
- `Sheet2`
|
||||
- 프로젝트 코드와 PM 이름 매핑으로 추정
|
||||
|
||||
### 3. `payment.csv`
|
||||
|
||||
용도:
|
||||
- 프로젝트별 수입/지출 전표
|
||||
|
||||
주요 컬럼:
|
||||
- `프로젝트코드`
|
||||
- `사업명`
|
||||
- `사업명(표출PJT)`
|
||||
- `사업명(인트라넷기준)`
|
||||
- `사업분야`
|
||||
- `세부분야`
|
||||
- `부서명`
|
||||
- `팀명`
|
||||
- `거래처`
|
||||
- `적요`
|
||||
- `차변공급가`
|
||||
- `대변공급가`
|
||||
- `지출`
|
||||
- `수입`
|
||||
- `구분`
|
||||
- `프로젝트성격`
|
||||
|
||||
## Recommended Model
|
||||
|
||||
### Raw Layer
|
||||
|
||||
원본을 그대로 적재하는 영역.
|
||||
|
||||
- `raw_organization_import`
|
||||
- `raw_mh_import`
|
||||
- `raw_payment_import`
|
||||
|
||||
원칙:
|
||||
- 원본 행을 최대한 손대지 않고 저장
|
||||
- 파일명, 업로드시각, 배치 ID 같이 저장
|
||||
|
||||
### Standard Layer
|
||||
|
||||
정규화된 운영 테이블.
|
||||
|
||||
#### Members
|
||||
|
||||
- `members`
|
||||
- `id`
|
||||
- `employee_id`
|
||||
- `name`
|
||||
- `company`
|
||||
- `rank`
|
||||
- `position`
|
||||
- `phone`
|
||||
- `email`
|
||||
- `active`
|
||||
|
||||
#### Organization
|
||||
|
||||
- `org_units`
|
||||
- `id`
|
||||
- `unit_type`
|
||||
- `name`
|
||||
- `parent_id`
|
||||
|
||||
- `member_org_assignments`
|
||||
- `id`
|
||||
- `member_id`
|
||||
- `part_name`
|
||||
- `group_name`
|
||||
- `division_name`
|
||||
- `team_name`
|
||||
- `cell_name`
|
||||
- `effective_from`
|
||||
- `effective_to`
|
||||
|
||||
#### Projects
|
||||
|
||||
- `projects`
|
||||
- `id`
|
||||
- `project_code`
|
||||
- `project_name`
|
||||
- `display_name`
|
||||
- `intranet_name`
|
||||
- `domain`
|
||||
- `subdomain`
|
||||
- `project_type`
|
||||
- `project_nature`
|
||||
|
||||
- `project_aliases`
|
||||
- `id`
|
||||
- `project_id`
|
||||
- `alias_type`
|
||||
- `alias_value`
|
||||
|
||||
- `project_pm_assignments`
|
||||
- `id`
|
||||
- `project_id`
|
||||
- `member_id`
|
||||
- `source`
|
||||
|
||||
#### Work Logs
|
||||
|
||||
- `work_logs`
|
||||
- `id`
|
||||
- `member_id`
|
||||
- `work_date`
|
||||
- `team_category`
|
||||
- `team_name`
|
||||
- `user_state`
|
||||
- `shift_hours`
|
||||
- `late_flag`
|
||||
|
||||
- `work_log_segments`
|
||||
- `id`
|
||||
- `work_log_id`
|
||||
- `project_id`
|
||||
- `activity_code`
|
||||
- `hours`
|
||||
- `is_overtime`
|
||||
- `slot_type`
|
||||
|
||||
#### Vouchers
|
||||
|
||||
- `vouchers`
|
||||
- `id`
|
||||
- `company_name`
|
||||
- `request_date`
|
||||
- `issue_date`
|
||||
- `issue_month`
|
||||
- `account_code`
|
||||
- `management_account_code`
|
||||
- `project_id`
|
||||
- `department_name`
|
||||
- `team_name`
|
||||
- `customer_name`
|
||||
- `summary`
|
||||
- `debit_amount`
|
||||
- `credit_amount`
|
||||
- `expense_amount`
|
||||
- `income_amount`
|
||||
- `voucher_type`
|
||||
- `project_nature`
|
||||
- `note`
|
||||
|
||||
#### Reference
|
||||
|
||||
- `member_cost_rates`
|
||||
- 직급별 표준 인건비
|
||||
|
||||
## Matching Rules
|
||||
|
||||
### Member Match
|
||||
|
||||
우선순위:
|
||||
1. `MH.xlsx.사원번호`
|
||||
2. `organization.xlsx.tag`
|
||||
3. 이름 단독 매칭은 보조 규칙으로만 사용
|
||||
|
||||
원칙:
|
||||
- `employee_id`가 있으면 그 값으로 병합
|
||||
- 이름만 같은 경우 자동 병합 금지
|
||||
|
||||
### Project Match
|
||||
|
||||
우선순위:
|
||||
1. `project_code`
|
||||
2. `사업명(인트라넷기준)`
|
||||
3. `사업명(표출PJT)`
|
||||
4. `프로젝트명`
|
||||
|
||||
원칙:
|
||||
- `project_code`를 정식 키로 사용
|
||||
- 이름 차이는 `project_aliases`로 흡수
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1
|
||||
|
||||
- `payment.html`, `mh.html`을 현재 대시보드 탭에 편입
|
||||
- 기존 HTML 기능은 유지
|
||||
- 파일은 backend route를 통해 iframe으로 연결
|
||||
|
||||
### Phase 2
|
||||
|
||||
- raw import 테이블 생성
|
||||
- 원본 3종 import 스크립트 작성
|
||||
- 파일별 업로드/재적재 배치 ID 관리
|
||||
|
||||
### Phase 3
|
||||
|
||||
- 표준 테이블 생성
|
||||
- raw -> standard 정규화 파이프라인 작성
|
||||
- 멤버/프로젝트 매핑 규칙 적용
|
||||
|
||||
### Phase 4
|
||||
|
||||
- `payment.html`, `mh.html`의 파일 직접 파싱 로직을 API 기반 조회로 전환
|
||||
- 프론트는 공통 DB 기준으로만 동작
|
||||
|
||||
## Immediate Next Tasks
|
||||
|
||||
1. Postgres 스키마 초안 SQL 작성
|
||||
2. `payment.csv` import 파서 작성
|
||||
3. `MH.xlsx` import 파서 작성
|
||||
4. `organization.xlsx` import 파서 작성
|
||||
5. 멤버/프로젝트 중복 병합 규칙 구현
|
||||
145
docs/NEXT_SESSION_CHECKPOINT.md
Normal file
145
docs/NEXT_SESSION_CHECKPOINT.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Next Session Checkpoint
|
||||
|
||||
## Current Base
|
||||
|
||||
- `8080` 공개 기준 브랜치: `total`
|
||||
- `8081` 작업 기준 브랜치: `work-8081`
|
||||
- `8080` 공개 기준 커밋: `637b390`
|
||||
- `8081` worktree 경로: `/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081`
|
||||
- `8081` 실제 서빙 책임 맵: [architecture/8081_SERVING_MAP.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/8081_SERVING_MAP.md)
|
||||
- 메인 히스토리: [DEVELOPMENT_HISTORY.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/DEVELOPMENT_HISTORY.md)
|
||||
- 작업 룰북: [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_RULEBOOK.md)
|
||||
- 실행 플로우: [WORK_EXECUTION_FLOW.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_EXECUTION_FLOW.md)
|
||||
- dev/prod DB 프로토콜: [DEV_PROD_DB_PROTOCOL.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/DEV_PROD_DB_PROTOCOL.md)
|
||||
- 회귀 체크리스트: [REGRESSION_CHECKLIST.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/REGRESSION_CHECKLIST.md)
|
||||
|
||||
## Mandatory Start Rule
|
||||
|
||||
당일 첫 작업 전에는 아래 순서를 먼저 확인한다.
|
||||
|
||||
1. 브랜치 기준 확인
|
||||
2. 열린 이슈 확인
|
||||
3. [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_RULEBOOK.md) 확인
|
||||
4. 이 문서 확인
|
||||
5. `git status`, 변경 파일, 미추적 파일 확인
|
||||
|
||||
주의:
|
||||
|
||||
- `8080` 기준 코드는 직접 수정하지 않는다.
|
||||
- 새 작업은 항상 `.dev-worktree-8081`에서 진행한다.
|
||||
- 커밋과 푸시는 사용자 지시가 있을 때만 수행한다.
|
||||
|
||||
## Confirmed Runtime Rule
|
||||
|
||||
- `8080`은 루트 workspace의 `total` 기준으로 유지한다.
|
||||
- `8081`은 `.dev-worktree-8081` + `work-8081` 기준으로만 수정한다.
|
||||
- `main`, `hyunho`는 보류 브랜치이며 현재 작업에 사용하지 않는다.
|
||||
- `8081` 변경을 `8080`에 올릴 때는 reviewed file diff 기준으로만 반영한다.
|
||||
- `8081` DB는 운영 정본이 아니라 `8080` 기준 검증용 복제본처럼 다룬다.
|
||||
|
||||
## What Was Stabilized
|
||||
|
||||
### Branch / Worktree Safety
|
||||
|
||||
- 기존 `8081` 작업본은 [`.dev-worktree-8081-backup-2026-04-01`](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081-backup-2026-04-01)로 보존
|
||||
- 현재 [`.dev-worktree-8081`](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081)는 `work-8081` 기준으로 재생성
|
||||
- `8080` 루트 workspace는 그대로 두고 분리 운영
|
||||
|
||||
### 8081 Design / Serving Baseline
|
||||
|
||||
- 디자인 SSOT 토큰:
|
||||
- [frontend/public/design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css)
|
||||
- 디자인 SSOT 패턴:
|
||||
- [frontend/public/design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css)
|
||||
- 디자인 기준 문서:
|
||||
- [architecture/DESIGN_SSOT.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/DESIGN_SSOT.md)
|
||||
- 로그인 기본 스타일은 [frontend/public/styles.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles.css) 기준으로 유지
|
||||
- `8081` 허브 전용 디자인은 [frontend/public/styles-8081-design.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles-8081-design.css)에서만 덮어씀
|
||||
- 조직현황은 [legacy/static/common.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/common.css), [legacy/static/organization.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/organization.css), [legacy/static/organization.js](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/organization.js)를 사용
|
||||
- 프로젝트별 분석 디자인은 [incoming-files/served/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/payment.html) 내부에서 `design-tokens.css` + `design-patterns.css`를 참조
|
||||
- 사업관리대장 상세 팝업 디자인은 [incoming-files/사업관리대장/ledger-override.js](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/사업관리대장/ledger-override.js)에서 `design-tokens.css` + `design-patterns.css`를 직접 링크
|
||||
|
||||
디자인 수정 우선순위:
|
||||
|
||||
1. [frontend/public/design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css)
|
||||
2. [frontend/public/design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css)
|
||||
3. 화면별 실제 서빙 파일
|
||||
|
||||
주의:
|
||||
|
||||
- `incoming-files/sample style.css`는 참고 기준이지만 직접 런타임 수정 파일이 아니다.
|
||||
- `incoming-files` 원본/reference 파일을 먼저 고치지 않는다.
|
||||
- 새 디자인 수정은 먼저 토큰/패턴 파일에서 해결 가능한지 확인한 뒤, 불가피할 때만 화면별 파일에 내린다.
|
||||
|
||||
### 1차 구조 정리 진행분
|
||||
|
||||
- 이슈 기준:
|
||||
- `#14` 전체 구조 정리 umbrella
|
||||
- `#18` 1차: 파일 책임 맵 정리 및 프런트 서빙 경로 정돈
|
||||
- `#19` 2차: 백엔드 라우터/서빙 책임 분리
|
||||
- `#20` 3차: worktree/스크립트/문서 정리
|
||||
- 책임 맵 문서 추가:
|
||||
- [architecture/8081_SERVING_MAP.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/8081_SERVING_MAP.md)
|
||||
- `/integrations/payment`, `/integrations/mh`의 실제 서빙 파일을 분리:
|
||||
- [incoming-files/served/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/payment.html)
|
||||
- [incoming-files/served/mh.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/mh.html)
|
||||
- 기존 [incoming-files/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/payment.html), [incoming-files/mh.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/mh.html)은 비교/복구용 복사본으로 당분간 유지
|
||||
- backend 서빙 경로는 [backend/app/main.py](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/backend/app/main.py)에서 `incoming-files/served/*`를 보도록 정리 시작
|
||||
|
||||
## Current Actual Serving Map
|
||||
|
||||
- `/`:
|
||||
- [frontend/public/index.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/index.html)
|
||||
- `/styles.css`:
|
||||
- [frontend/public/styles.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles.css)
|
||||
- `/styles-8081-design.css`:
|
||||
- [frontend/public/styles-8081-design.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/styles-8081-design.css)
|
||||
- `/legacy/organization`:
|
||||
- [legacy/static/DashBoard-organization.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/legacy/static/DashBoard-organization.html)
|
||||
- `/integrations/payment`:
|
||||
- [incoming-files/served/payment.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/payment.html)
|
||||
- `/integrations/mh`:
|
||||
- [incoming-files/served/mh.html](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/served/mh.html)
|
||||
|
||||
## Cross Checks Last Confirmed
|
||||
|
||||
- `8080`: `curl http://localhost:8080/api/health` 정상
|
||||
- `8081` dev 컨테이너: proxy/backend/frontend/db `healthy`
|
||||
- `8081` backend 내부 확인:
|
||||
- `/api/health` 200
|
||||
- `/legacy/organization` 200
|
||||
- `/integrations/payment` 200
|
||||
- `/integrations/mh` 200
|
||||
- `incoming-files/served` 내 실제 서빙 파일 존재 확인
|
||||
|
||||
주의:
|
||||
|
||||
- Codex 터미널 세션에서는 `curl http://localhost:8081`가 간헐적으로 실패할 수 있다.
|
||||
- 이 경우 브라우저 확인 또는 컨테이너 내부 라우트 확인을 기준으로 판단한다.
|
||||
|
||||
## Open Issues Relevant Now
|
||||
|
||||
- `#14` 누적된 임시 로직 정리 및 중복 코드 제거
|
||||
- `#16` 사업관리대장 메인 연동 및 기본 원본 DB화
|
||||
- `#17` 8081 분리 worktree 기동 절차와 로컬 디자인 자산 복제 고정
|
||||
- `#18` 8081 파일 책임 맵 정리 및 프런트 서빙 경로 정돈
|
||||
- `#19` 8081 백엔드 라우터/서빙 책임 분리
|
||||
- `#20` 8081 worktree 준비 스크립트·문서·운영 규칙 정리
|
||||
|
||||
## Recommended Next Work Order
|
||||
|
||||
1. `#18` 범위에서 실제 서빙 파일과 비교용 파일 경계를 더 명확히 정리
|
||||
2. 사업관리대장 탭 기능 추가 전에 수정 대상 파일을 고정
|
||||
3. 그 다음 `#19`로 backend 라우터/서빙 책임 분리
|
||||
4. 마지막으로 `#20`에서 스크립트/문서/운영 규칙 정리
|
||||
|
||||
## Quick Resume Prompt
|
||||
|
||||
다음 세션 시작 시 아래 기준으로 이어가면 된다.
|
||||
|
||||
- `8080` 기준은 `total`
|
||||
- `8081` 작업은 `work-8081` + `.dev-worktree-8081`
|
||||
- 먼저 [WORK_RULEBOOK.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/WORK_RULEBOOK.md), [NEXT_SESSION_CHECKPOINT.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/NEXT_SESSION_CHECKPOINT.md), [architecture/8081_SERVING_MAP.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/8081_SERVING_MAP.md) 확인
|
||||
- 디자인 수정이면 [frontend/public/design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css), [frontend/public/design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css), [architecture/DESIGN_SSOT.md](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/docs/architecture/DESIGN_SSOT.md) 먼저 확인
|
||||
- 현재 1차 구조 정리 기준 이슈는 `#18`
|
||||
- 작업 전 `git status`, dev 컨테이너 상태, `/api/health`, `/legacy/organization`, `/integrations/payment`, `/integrations/mh`를 먼저 확인
|
||||
162
docs/REGRESSION_CHECKLIST.md
Normal file
162
docs/REGRESSION_CHECKLIST.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# 회귀 검증 체크리스트
|
||||
|
||||
## 목적
|
||||
|
||||
- 새 기능을 추가하거나 기존 기능을 수정할 때, 이전에 되던 핵심 기능이 깨졌는지 빠르게 확인한다.
|
||||
- `8081` 작업용에서 검증한 결과를 신뢰할 수 있도록 `환경`, `데이터`, `핵심 시나리오`를 고정한다.
|
||||
- 완료 판단을 감이 아니라 반복 가능한 체크 절차로 바꾼다.
|
||||
|
||||
## 적용 원칙
|
||||
|
||||
- 코드 수정은 먼저 `8081`에서 수행한다.
|
||||
- 데이터 기준은 항상 `8080` 공개용 DB를 따른다.
|
||||
- 검증 전에는 작업 범위에 맞는 DB 동기화를 먼저 수행한다.
|
||||
- 기능 수정 후에는 관련 화면만 보지 말고, 이 문서의 핵심 시나리오를 함께 확인한다.
|
||||
|
||||
관련 문서:
|
||||
|
||||
- [DEV_PROD_DB_PROTOCOL.md](/home/hyunho/projects/mh-dashboard-organization/docs/DEV_PROD_DB_PROTOCOL.md)
|
||||
- [INFRA_VALIDATION_CHECKLIST.md](/home/hyunho/projects/mh-dashboard-organization/docs/INFRA_VALIDATION_CHECKLIST.md)
|
||||
|
||||
## 작업 시작 전
|
||||
|
||||
### 1. 서버 상태 확인
|
||||
|
||||
- `8081` 작업용 접속 확인
|
||||
- `8080` 공개용 접속 확인
|
||||
- `docker compose ps`에서 `backend`, `frontend`, `proxy`, `db`가 정상인지 확인
|
||||
- `8081`은 기본적으로 `./scripts/start_8081.sh` 또는 `./scripts/prepare_dev_worktree.sh` 후 `.dev-worktree-8081` 에서 `docker compose -p mh-dashboard-organization-dev --env-file .env -f docker-compose.8081.yml up -d --build` 로 기동
|
||||
- `8081` 기동 후 `docker inspect mh-dashboard-organization-dev-backend-1`에서 마운트 경로가 `.dev-worktree-8081/...`인지 확인
|
||||
|
||||
### 2. 데이터 동기화 범위 결정
|
||||
|
||||
- 조직도, 관리자모드, 자리배치도 작업 전:
|
||||
- `./scripts/sync_prod_db_to_dev.sh minimal`
|
||||
- 프로젝트별 분석, 팀/개인별 분석 작업 전:
|
||||
- `./scripts/sync_prod_db_to_dev.sh analysis`
|
||||
- 공개용 기준 전체 데이터 재검증이 필요한 경우만:
|
||||
- `./scripts/sync_prod_db_to_dev.sh full`
|
||||
|
||||
### 3. 기준 고정
|
||||
|
||||
- 어느 서버에서 재현했는지 기록
|
||||
- 어떤 데이터 동기화 범위로 검증했는지 기록
|
||||
- 브라우저 캐시 영향을 피하려면 강력 새로고침 후 확인
|
||||
|
||||
## 공통 회귀 시나리오
|
||||
|
||||
기능 수정 후 아래 항목을 최소한 확인한다.
|
||||
|
||||
### A. 허브 및 공통 진입
|
||||
|
||||
- 메인 허브가 정상 렌더링된다.
|
||||
- 상단 탭 이동이 정상 동작한다.
|
||||
- 로그인 상태가 비정상적으로 풀리지 않는다.
|
||||
|
||||
### B. 조직현황
|
||||
|
||||
- 조직도 트리가 정상 표시된다.
|
||||
- 관리자모드 진입이 가능하다.
|
||||
- 대상인원 클릭 시 기본정보 모달이 열린다.
|
||||
- `+` 신규 구성원 추가 모달이 열린다.
|
||||
- 기본정보 저장이 정상 동작한다.
|
||||
|
||||
### C. 자리배치도
|
||||
|
||||
- `기술개발센터`, `한맥빌딩 6층`, `한맥빌딩 7층` 도면이 모두 열린다.
|
||||
- 미배치 인원 목록이 정상 표시된다.
|
||||
- 미배치 인원을 chair에 드래그앤드롭할 수 있다.
|
||||
- 드롭 후:
|
||||
- 미배치 목록에서 사라진다.
|
||||
- chair에 배치 상태가 표시된다.
|
||||
- 이름/직급 표기가 보인다.
|
||||
- 배치된 좌석 클릭 후 해제 또는 수정 흐름이 정상 동작한다.
|
||||
|
||||
### D. 조직도와 자리배치 연동
|
||||
|
||||
- 조직도에서 인원 클릭 시 상세 정보가 열린다.
|
||||
- 재석위치 미리보기가 표시된다.
|
||||
- 좌석이 배정된 인원은 해당 자리로 줌인된다.
|
||||
|
||||
### E. 프로젝트별 분석
|
||||
|
||||
- 월 선택이 정상 동작한다.
|
||||
- 프로젝트 목록과 합계가 비어 있지 않다.
|
||||
- `1월`, `2월` 데이터가 현재 기준값과 일치한다.
|
||||
|
||||
현재 기준 검증값:
|
||||
|
||||
- `2026-01`
|
||||
- 시간 `37,342.39`
|
||||
- 인건비 `1,391,966,625`
|
||||
- `2026-02`
|
||||
- 시간 `29,060.59`
|
||||
- 인건비 `1,078,337,651`
|
||||
|
||||
### F. 팀/개인별 분석
|
||||
|
||||
- `전체`, `GPD`, `TDC` 버튼이 순서대로 보인다.
|
||||
- `전체`에서 모든 팀이 노출된다.
|
||||
- `GPD`, `TDC` 선택 시 각 소속 범위만 버튼 기준으로 보인다.
|
||||
- 검색은 버튼 상태와 무관하게 전체 데이터를 검색한다.
|
||||
|
||||
## 작업 유형별 필수 추가 확인
|
||||
|
||||
### 조직도 / 관리자모드 수정 시
|
||||
|
||||
- 대상인원 수정 모달 레이아웃이 깨지지 않는지 확인
|
||||
- 신규 구성원 추가 모달도 같은 레이아웃으로 보이는지 확인
|
||||
- 저장 후 목록 반영이 정상인지 확인
|
||||
|
||||
### 자리배치도 수정 시
|
||||
|
||||
- viewer iframe 로드 여부 확인
|
||||
- 드래그앤드롭 이후 배치 상태가 즉시 반영되는지 확인
|
||||
- 조직도 상세 재석위치 preview까지 같이 확인
|
||||
|
||||
### 분석 로직 수정 시
|
||||
|
||||
- 작업 전에 반드시 `analysis` 또는 `full` 동기화 수행
|
||||
- 월별 합계 검증값 재확인
|
||||
- 원본 기준과 차이가 있으면 반올림, 제외 인원, 가공시간 규칙부터 점검
|
||||
|
||||
## 완료 처리 기준
|
||||
|
||||
수정 사항을 완료로 판단하려면 아래를 모두 만족해야 한다.
|
||||
|
||||
- 수정한 기능이 의도대로 동작한다.
|
||||
- 관련 공통 회귀 시나리오가 깨지지 않는다.
|
||||
- 필요한 경우 `8081`에서 검증 결과를 숫자 또는 화면 기준으로 기록한다.
|
||||
- 이후에만 `8080` 공개용 반영 여부를 판단한다.
|
||||
|
||||
## 장애 원인 분류 기준
|
||||
|
||||
문제가 생기면 먼저 아래 셋 중 어디인지 분리한다.
|
||||
|
||||
- 코드 차이
|
||||
- `8080`, `8081`의 정적 파일 또는 백엔드 로직이 다름
|
||||
- DB 차이
|
||||
- `members`, `seat_maps`, `integration_*` 등 기준 데이터가 다름
|
||||
- 캐시 또는 런타임 상태
|
||||
- 정적 파일 캐시, 컨테이너 재시작 미반영, 브라우저 세션 상태 문제
|
||||
|
||||
이 분류를 먼저 해야 원인을 잘못 짚지 않는다.
|
||||
|
||||
## 권장 기록 방식
|
||||
|
||||
작업 종료 시 아래 형식으로 남긴다.
|
||||
|
||||
```text
|
||||
작업 범위:
|
||||
- 예: 조직현황 관리자모드 기본정보 모달 레이아웃 변경
|
||||
|
||||
검증 환경:
|
||||
- 서버: 8081
|
||||
- DB 동기화: minimal / analysis / full 중 무엇을 사용했는지
|
||||
|
||||
검증 결과:
|
||||
- 조직도: 정상
|
||||
- 관리자모드 모달: 정상
|
||||
- 자리배치도 연동: 정상 또는 미검증
|
||||
- 프로젝트별 분석: 정상 또는 미검증
|
||||
```
|
||||
143
docs/TODAY_WORK_PREP_2026-03-30.md
Normal file
143
docs/TODAY_WORK_PREP_2026-03-30.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Today Work Prep - 2026-03-30
|
||||
|
||||
## Current Local State
|
||||
|
||||
- working branch: `total`
|
||||
- HEAD: `24852d4` (`Fix seatmap slot matching and update member modal layout`)
|
||||
- remote tracking: `origin/total`
|
||||
- status: local branch is `ahead 2`
|
||||
- open PRs: none
|
||||
|
||||
untracked files:
|
||||
|
||||
- `docs/HISTORY_ASOF_DB_PLAN.md`
|
||||
- `incoming-files/6f.html`
|
||||
- `incoming-files/7f.html`
|
||||
- `incoming-files/center.html`
|
||||
|
||||
주의:
|
||||
|
||||
- `docs/NEXT_SESSION_CHECKPOINT.md` 의 최신 checked commit 은 아직 `1d15cf9` 로 남아 있다.
|
||||
- 실제 최신 작업 판단은 아래 최근 2개 로컬 커밋 기준으로 보는 것이 맞다.
|
||||
|
||||
## What Was Added After `origin/total`
|
||||
|
||||
### Commit `d666141`
|
||||
|
||||
- 3개 고정 오피스 자리배치도 반영
|
||||
- `technical-development-center`
|
||||
- `hanmac-building-6f`
|
||||
- `hanmac-building-7f`
|
||||
- 백엔드 `office_key` 기반 active viewer/layout 조회 지원
|
||||
- 프런트 자리배치도 탭에서 3개 오피스 선택 지원
|
||||
- `scripts/sync_prod_db_to_dev.sh` 추가
|
||||
- `docs/DEV_PROD_DB_PROTOCOL.md` 추가
|
||||
|
||||
### Commit `24852d4`
|
||||
|
||||
- slot 기반 자리 저장 시 slot matching 보정
|
||||
- 멤버 상세 모달 / 조직도 seat preview 레이아웃 조정
|
||||
- 회귀 점검용 `docs/REGRESSION_CHECKLIST.md` 추가
|
||||
- dev/prod sync script 후속 보정
|
||||
|
||||
## Remote Branch / Issue Snapshot
|
||||
|
||||
remote branches:
|
||||
|
||||
- `total` -> `1d15cf9`
|
||||
- `hyunho` -> `8efb5da`
|
||||
- `main` -> `7a0bd54`
|
||||
|
||||
open issues:
|
||||
|
||||
- `#11` `[P0] [버그] 자리배치도 회귀 오류`
|
||||
- `#12` `[P1] [DB] 공개용/작업용 seat_positions 스키마 불일치 정리`
|
||||
- `#13` `[P1] [인프라] 작업용 DB 동기화 절차 안정화 및 자동화`
|
||||
- `#14` `[P2] [리팩터링] 누적된 임시 로직 정리 및 중복 코드 제거`
|
||||
- `#10` `[P1] [분석] 1~2월 원본 정합성 보정 및 팀/개인별 검색 범위 개선 작업 정리`
|
||||
- `#9` `[P1] [이력관리] as-of date / 버전 누적 저장`
|
||||
- `#8` `[P2] [자리배치도] 좌석 클릭 시 개인 상위 조직 트리 표시`
|
||||
- `#7` `[P2] [자리배치도] 팀별 색상 오버레이 표시`
|
||||
- `#5` `[P2] [인증] 권한 제어 마무리 및 mock login 정리`
|
||||
- `#3` `[P1] [기능] 사무실 좌석 배치도 조회 및 관리자 편집 기능 고도화`
|
||||
- `#2` `[P0] [인프라] 백엔드 영속 저장 구조 운영 마무리`
|
||||
|
||||
현재 관계 해석:
|
||||
|
||||
- `#11` 은 최근 2개 커밋이 직접 겨냥한 회귀 묶음이다.
|
||||
- `#12`, `#13` 은 `#11` 재발 방지용 운영 과제에 가깝다.
|
||||
- `#3` 은 다중 오피스 도면 반영으로 많이 진척됐지만, 공개용 기준 회귀 검증 전에는 완료 처리하면 안 된다.
|
||||
- `#2` 는 단순 구현보다 dev/prod 데이터 운영 기준 정리가 핵심으로 바뀌었다.
|
||||
- `#5` 는 로그인 구현보다 권한 경계와 `/api/mock-login` 정리가 남은 상태다.
|
||||
|
||||
## Best Starting Point Today
|
||||
|
||||
오늘 첫 작업은 새 기능 추가보다, 최근 자리배치도/DB 동기화 작업을 검증 가능한 상태로 굳히는 쪽이 우선이다.
|
||||
|
||||
우선순위:
|
||||
|
||||
1. `#13` 프로토콜대로 작업용 DB를 `minimal` 범위로 동기화
|
||||
2. `docs/REGRESSION_CHECKLIST.md` 기준으로 자리배치도 회귀 확인
|
||||
3. 최근 2개 로컬 커밋을 `origin/total` 에 올릴지 결정
|
||||
4. 회귀가 남아 있으면 `#11` 계속, 없으면 `#5` 또는 `#12/#13` 후속 정리로 이동
|
||||
|
||||
이 순서가 맞는 이유:
|
||||
|
||||
- 현재 가장 최근 변경이 seatmap + DB sync 쪽에 몰려 있다.
|
||||
- 원격 `total` 은 아직 해당 수정들을 포함하지 않는다.
|
||||
- 검증 없이 다른 기능으로 넘어가면 회귀 원인과 신규 작업이 다시 섞인다.
|
||||
|
||||
## Concrete Start Checklist
|
||||
|
||||
세션 시작 즉시:
|
||||
|
||||
1. `docs/DEV_PROD_DB_PROTOCOL.md` 다시 확인
|
||||
2. 필요 시 `./scripts/sync_prod_db_to_dev.sh minimal`
|
||||
3. 로그인 상태 확인
|
||||
4. 아래 3개를 오피스별로 확인
|
||||
- 관리자 DnD 배치 저장
|
||||
- 조직도 상세 seat preview
|
||||
- 비관리자 seatmap 진입 / 표시
|
||||
|
||||
필수 확인 오피스:
|
||||
|
||||
- `기술개발센터`
|
||||
- `한맥빌딩 6층`
|
||||
- `한맥빌딩 7층`
|
||||
|
||||
## Recommended Decision Tree
|
||||
|
||||
### Case A. 회귀가 남아 있음
|
||||
|
||||
- 바로 `#11` 우선
|
||||
- 동시에 원인 범주를 분리
|
||||
- DB sync 실패
|
||||
- `seat_positions` 스키마 차이
|
||||
- 프런트 fallback 오류
|
||||
- 저장 API 로직 오류
|
||||
|
||||
### Case B. 회귀가 해소됨
|
||||
|
||||
- 최근 2개 커밋 푸시
|
||||
- Gitea `#11`, `#3`, `#2` 코멘트 상태 업데이트
|
||||
- 다음 메인 작업을 아래 중 하나로 선택
|
||||
- `#5` 권한 제어 / mock login 제거
|
||||
- `#12`, `#13` DB sync 안정화 마무리
|
||||
- `#9` history / as-of 구조 착수
|
||||
|
||||
## Suggested Main Task After Verification
|
||||
|
||||
가장 자연스러운 다음 메인 작업은 `#5` 보다 `#12`, `#13` 마무리다.
|
||||
|
||||
이유:
|
||||
|
||||
- 지금 이 코드베이스에서 자리배치도/조직도 검증은 DB 상태에 크게 좌우된다.
|
||||
- 권한 작업을 시작해도 검증 기반이 흔들리면 다시 혼선이 생긴다.
|
||||
- 반대로 sync 절차와 스키마 호환을 먼저 고정하면 이후 `#5`, `#9`, `#8`, `#7` 진행이 쉬워진다.
|
||||
|
||||
## Short Summary
|
||||
|
||||
- 코드 최신 상태는 로컬 `total@24852d4`
|
||||
- 원격 `total` 은 아직 최신 seatmap/sync 수정 전 상태
|
||||
- 오늘 첫 목표는 `#11` 관련 회귀 검증과 `#12/#13` 기반 정리
|
||||
- 검증 완료 전에는 새 기능보다 seatmap + DB 운영 안정화를 우선하는 것이 맞다
|
||||
269
docs/WORK_EXECUTION_FLOW.md
Normal file
269
docs/WORK_EXECUTION_FLOW.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Work Execution Flow
|
||||
|
||||
## 목적
|
||||
|
||||
이 문서는 앞으로 이 프로젝트에서 작업을 어떤 순서로 진행해야 하는지 아주 쉽게 고정하기 위한 문서다.
|
||||
|
||||
세미나에서 들은 흐름을 이 프로젝트 기준으로 다시 쓰면 아래 순서다.
|
||||
|
||||
1. `SSOT` 먼저 확인
|
||||
2. 이슈 생성 또는 연결
|
||||
3. 완료조건 먼저 적기
|
||||
4. 실행 계획 적기
|
||||
5. 필요한 동기화 먼저 하기
|
||||
6. 코드 수정 / 화면 작업 수행
|
||||
7. 가드레일 테스트
|
||||
8. 기록 남기기
|
||||
|
||||
이 순서를 지키는 이유는 하나다.
|
||||
|
||||
- 작업 도중 기준이 바뀌지 않게 하기
|
||||
- 임시 연결이 누적되지 않게 하기
|
||||
- 나중에 봐도 왜 이렇게 했는지 알 수 있게 하기
|
||||
- `8081` 작업이 `8080`을 망가뜨리지 않게 하기
|
||||
|
||||
## 1. SSOT 먼저 확인
|
||||
|
||||
`SSOT`는 Single Source Of Truth 의 줄임말이다.
|
||||
|
||||
쉬운 말로:
|
||||
|
||||
- "무엇을 기준 진실로 볼 것인가"
|
||||
|
||||
이걸 먼저 정하지 않으면 작업 중간에 기준이 계속 바뀌어서 코드가 꼬인다.
|
||||
|
||||
이 프로젝트에서 자주 쓰는 SSOT:
|
||||
|
||||
- 공개용 코드 기준: `/home/hyunho/projects/mh-dashboard-organization`
|
||||
- 작업용 코드 기준: `/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081`
|
||||
- 데이터 정본 기준: `8080` DB
|
||||
- 기능 검증 기준: `8081`
|
||||
- 사업관리대장 디자인 기준: `MH 통합 대시보드_260320.html`
|
||||
- 허브 공통 시각 언어 기준: `sample style.css`
|
||||
- 런타임 디자인 토큰 기준: `frontend/public/design-tokens.css`
|
||||
- 런타임 디자인 패턴 기준: `frontend/public/design-patterns.css`
|
||||
- 현재 작업 지시 기준: 연결된 Gitea 이슈
|
||||
|
||||
작업 시작 전에 먼저 정해야 하는 질문:
|
||||
|
||||
- 이번 작업의 코드 기준은 어디인가?
|
||||
- 이번 작업의 데이터 기준은 어디인가?
|
||||
- 이번 화면의 디자인 기준 파일은 무엇인가?
|
||||
- 지금 바꾸려는 화면이 실제로 어떤 파일에서 렌더링되는가?
|
||||
|
||||
이걸 모르고 코드를 건드리면 높은 확률로 엉뚱한 파일을 수정하게 된다.
|
||||
|
||||
디자인 작업 추가 규칙:
|
||||
|
||||
- 디자인 수정은 항상 `design-tokens.css`와 `design-patterns.css`를 먼저 확인한다.
|
||||
- 색/패널/버튼/테이블/팝업이 공통 규칙으로 해결 가능한지 먼저 본다.
|
||||
- 해결 가능하면 화면별 파일을 고치지 않고 토큰/패턴 파일에서 수정한다.
|
||||
- 화면별 실제 서빙 파일은 마지막 단계에서만 조정한다.
|
||||
- 원본/reference 파일은 비교용이지 직접 수정 우선 대상이 아니다.
|
||||
|
||||
## 2. 이슈 생성 또는 연결
|
||||
|
||||
작업은 이슈 없이 하지 않는다.
|
||||
|
||||
이유:
|
||||
|
||||
- 왜 하는 작업인지 남기기 위해
|
||||
- 중간에 범위가 커지는 걸 막기 위해
|
||||
- 다음 세션에서 바로 이어가기 위해
|
||||
|
||||
좋은 이슈는 아래 4개가 있어야 한다.
|
||||
|
||||
1. 배경
|
||||
2. 목표
|
||||
3. 현재 상태
|
||||
4. 남은 작업
|
||||
|
||||
이슈는 길게 쓸 필요는 없다.
|
||||
하지만 최소한 아래는 있어야 한다.
|
||||
|
||||
- 왜 이 작업을 하는지
|
||||
- 어디까지가 이번 범위인지
|
||||
- 무엇을 완료로 볼지
|
||||
|
||||
## 3. 완료조건 먼저 적기
|
||||
|
||||
이 단계가 중요하다.
|
||||
|
||||
완료조건이 없으면 "대충 된 것 같음" 상태에서 끝나기 쉽다.
|
||||
|
||||
좋은 완료조건 예시:
|
||||
|
||||
- `8081`이 `.dev-worktree-8081`를 실제로 마운트한다
|
||||
- `사업관리대장` 탭이 원본 기준 레이아웃으로 열린다
|
||||
- `8080`은 영향 없이 유지된다
|
||||
- 관련 회귀 검증을 통과한다
|
||||
|
||||
나쁜 완료조건 예시:
|
||||
|
||||
- 화면이 좀 괜찮아 보인다
|
||||
- 아마 될 것 같다
|
||||
- 코드 정리함
|
||||
|
||||
완료조건은 반드시 확인 가능한 문장이어야 한다.
|
||||
|
||||
즉:
|
||||
|
||||
- "봤을 때 예쁨"이 아니라
|
||||
- "어떤 URL에서 어떤 동작이 확인됨"이어야 한다
|
||||
|
||||
## 4. 실행 계획 적기
|
||||
|
||||
계획은 길 필요 없다.
|
||||
|
||||
이 프로젝트에서는 보통 아래 정도면 충분하다.
|
||||
|
||||
1. 기준 파일과 현재 연결 구조 확인
|
||||
2. `8081` worktree 기준으로만 수정
|
||||
3. 필요한 데이터 동기화
|
||||
4. 화면/기능 수정
|
||||
5. 회귀 검증
|
||||
6. 이슈 코멘트와 체크포인트 기록
|
||||
|
||||
핵심은:
|
||||
|
||||
- 수정 전에 먼저 구조를 파악하고
|
||||
- 범위를 정하고
|
||||
- 검증까지 포함해서 끝내는 것
|
||||
|
||||
## 5. 실행 전 동기화
|
||||
|
||||
이 프로젝트는 코드만 맞아도 안 되고, 데이터도 맞아야 한다.
|
||||
|
||||
그래서 실행 전에 동기화가 필요할 수 있다.
|
||||
|
||||
무슨 뜻이냐면:
|
||||
|
||||
- `8081`에서 기능 확인을 하더라도
|
||||
- 데이터가 `8080`과 다르면 검증 결과를 신뢰하면 안 된다
|
||||
|
||||
자주 쓰는 규칙:
|
||||
|
||||
- 조직도 / 멤버 / 자리배치 검증 전
|
||||
- `./scripts/sync_prod_db_to_dev.sh minimal`
|
||||
- 분석 화면까지 공개용 기준으로 맞춰야 할 때
|
||||
- `./scripts/sync_prod_db_to_dev.sh full`
|
||||
|
||||
또 코드 동기화도 중요하다.
|
||||
|
||||
- `8081`은 메인 workspace에서 직접 띄우지 않는다
|
||||
- 먼저 `./scripts/prepare_dev_worktree.sh`
|
||||
- 그 다음 `.dev-worktree-8081`에서 실행
|
||||
|
||||
즉 이 프로젝트의 동기화는 두 종류다.
|
||||
|
||||
- DB 동기화
|
||||
- 코드/worktree 동기화
|
||||
|
||||
## 6. 실제 실행
|
||||
|
||||
이 단계가 코드를 고치는 단계다.
|
||||
|
||||
하지만 여기서도 규칙이 있다.
|
||||
|
||||
- `8081`에서 먼저 작업
|
||||
- 기준 파일이 아닌 곳은 건드리지 않기
|
||||
- 임시 우회 연결을 만들었으면 반드시 기록 남기기
|
||||
- 연결 구조가 난잡해지면 바로 이슈에 `코드 정리 필요`를 남기기
|
||||
|
||||
특히 이 프로젝트는 아래가 자주 꼬인다.
|
||||
|
||||
- `frontend/public`
|
||||
- `legacy/static`
|
||||
- `incoming-files`
|
||||
- 정적 HTML
|
||||
- iframe 연결
|
||||
- 버전 쿼리스트링
|
||||
|
||||
그래서 실행 중 계속 확인해야 한다.
|
||||
|
||||
- 지금 내가 고친 파일이 실제 서빙 파일이 맞는가?
|
||||
- 지금 수정이 `8081` 전용인가, `8080` 공통인가?
|
||||
- 이 연결은 임시인가, 기준 구조인가?
|
||||
|
||||
## 7. 가드레일 테스트
|
||||
|
||||
가드레일 테스트는 쉬운 말로:
|
||||
|
||||
- "이 수정 때문에 같이 망가지면 안 되는 것들을 확인하는 테스트"
|
||||
|
||||
즉 핵심 기능만 보는 게 아니라, 같이 깨지기 쉬운 주변 기능까지 확인하는 것이다.
|
||||
|
||||
이 프로젝트에서 가드레일 테스트 예시:
|
||||
|
||||
- `8081` 디자인 수정 후
|
||||
- `8080`은 그대로인지 확인
|
||||
- 조직현황 수정 후
|
||||
- 조직도 iframe, 모달, 리스트뷰, seat preview 확인
|
||||
- 자리배치 수정 후
|
||||
- 관리자 저장
|
||||
- 비관리자 조회
|
||||
- 조직도 상세 seat preview
|
||||
- 분석 화면 수정 후
|
||||
- 기간 필터
|
||||
- 프로젝트/팀 전환
|
||||
- 빈 데이터 상태
|
||||
- 스타일 깨짐 여부
|
||||
|
||||
가드레일 테스트는 "다 테스트한다"가 아니다.
|
||||
|
||||
이번 수정 때문에 같이 깨질 가능성이 높은 것만 빠르게 확인하는 것이다.
|
||||
|
||||
## 8. 기록 남기기
|
||||
|
||||
작업은 기록까지 남겨야 끝난다.
|
||||
|
||||
남겨야 하는 것:
|
||||
|
||||
- 무엇을 바꿨는지
|
||||
- 무엇을 기준으로 했는지
|
||||
- 무엇을 검증했는지
|
||||
- 무엇이 아직 안 끝났는지
|
||||
- 다음에 어디서 이어야 하는지
|
||||
|
||||
남길 위치:
|
||||
|
||||
- Gitea 이슈 코멘트
|
||||
- 체크포인트 문서
|
||||
- 필요하면 룰북/프로토콜 문서
|
||||
|
||||
## 이 프로젝트용 한 줄 버전
|
||||
|
||||
앞으로는 아래 순서로 생각하면 된다.
|
||||
|
||||
1. 기준 진실부터 정한다
|
||||
2. 이슈에 작업 목적과 완료조건을 적는다
|
||||
3. 실행 전에 코드/DB 동기화를 맞춘다
|
||||
4. `8081`에서만 수정한다
|
||||
5. 같이 깨지면 안 되는 것까지 확인한다
|
||||
6. 결과를 기록한다
|
||||
|
||||
## 시작할 때 바로 쓰는 짧은 템플릿
|
||||
|
||||
작업 시작 전에 아래 6줄만 적어도 된다.
|
||||
|
||||
- SSOT:
|
||||
- 코드 기준:
|
||||
- 데이터 기준:
|
||||
- 디자인 기준:
|
||||
- 이슈:
|
||||
- 완료조건:
|
||||
- 계획:
|
||||
- 필요한 동기화:
|
||||
- 가드레일 테스트:
|
||||
|
||||
예시:
|
||||
|
||||
- SSOT:
|
||||
- 코드 기준: `/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081`
|
||||
- 데이터 기준: `8080` DB를 sync한 `8081`
|
||||
- 디자인 기준: `MH 통합 대시보드_260320.html`
|
||||
- 이슈: `#16`
|
||||
- 완료조건: `8081`에서 사업관리대장 메인이 원본 톤으로 열리고 `8080`은 안 바뀜
|
||||
- 계획: 연결 확인 → worktree 수정 → 검증 → 이슈 기록
|
||||
- 필요한 동기화: `minimal`
|
||||
- 가드레일 테스트: `8080 유지`, `조직현황 탭`, `프로젝트/팀 탭`
|
||||
285
docs/WORK_RULEBOOK.md
Normal file
285
docs/WORK_RULEBOOK.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# Work Rulebook
|
||||
|
||||
## Purpose
|
||||
|
||||
이 문서는 이 프로젝트에서 매일 작업을 시작하고 마무리할 때 반드시 따를 운영 규칙을 고정하기 위한 룰북이다.
|
||||
|
||||
목표는 아래 4가지다.
|
||||
|
||||
- 완료된 기능의 회귀 방지
|
||||
- 코드 문제와 DB 문제의 혼선 방지
|
||||
- 작업 기록 누락 방지
|
||||
- 매일 같은 기준으로 안정적으로 이어서 작업
|
||||
|
||||
## Rule 0. Morning Start Mandatory Check
|
||||
|
||||
이 규칙은 강제 규칙이다.
|
||||
|
||||
매일 아침 또는 그날의 첫 작업을 시작할 때는, 코드를 수정하기 전에 반드시 아래 순서를 먼저 수행한다.
|
||||
|
||||
1. Gitea 브랜치 상태 확인
|
||||
2. 열린 이슈 확인
|
||||
3. 이 문서 `WORK_RULEBOOK.md` 확인
|
||||
4. 최신 체크포인트 문서 확인
|
||||
5. 현재 워크트리의 미푸시 커밋, 변경 파일, 미추적 파일 확인
|
||||
|
||||
위 5단계를 확인하기 전에는 새 코드 작성, 기존 코드 수정, 임의 테스트 진행을 시작하지 않는다.
|
||||
|
||||
즉:
|
||||
|
||||
- "오늘 첫 작업"의 시작점은 코드 수정이 아니라 상태 확인이다.
|
||||
- 이 절차를 건너뛰고 바로 수정 작업에 들어가는 것은 금지한다.
|
||||
|
||||
추가 기준:
|
||||
|
||||
- 실제 작업 순서는 [WORK_EXECUTION_FLOW.md](/home/hyunho/projects/mh-dashboard-organization/docs/WORK_EXECUTION_FLOW.md) 를 따른다.
|
||||
- 특히 `SSOT → 이슈 → 완료조건 → 계획 → 동기화 → 실행 → 가드레일 테스트 → 기록` 순서를 기본 운영 흐름으로 본다.
|
||||
|
||||
## Rule 1. Completed Feature Protection
|
||||
|
||||
완료 판정된 작업물의 기능과 코드는 함부로 건드리지 않는다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- 직접 관련된 이슈가 없으면 완료 기능을 수정하지 않는다.
|
||||
- 완료 기능 수정이 필요하면 먼저 이유와 영향 범위를 이슈 또는 코멘트에 남긴다.
|
||||
- 단순 편의상 구조를 바꾸거나 정리하는 리팩터링으로 완료 기능 동작을 바꾸지 않는다.
|
||||
- 완료 기능을 수정한 경우에는 관련 회귀 검증까지 완료해야 한다.
|
||||
|
||||
핵심 원칙:
|
||||
|
||||
- "고치는 김에 같이 정리"를 금지한다.
|
||||
- 수정 범위는 현재 작업 목적에 필요한 최소 범위로 제한한다.
|
||||
|
||||
## Rule 2. Work Must Be Tied To An Issue
|
||||
|
||||
원칙적으로 이슈 없는 작업은 하지 않는다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- 모든 작업은 기존 이슈에 연결하거나 새 이슈/작업 메모를 만든 뒤 시작한다.
|
||||
- 왜 하는 작업인지 한 줄로라도 남긴다.
|
||||
- 임시 대응도 예외가 아니다.
|
||||
|
||||
## Rule 3. Branch And Workspace Awareness
|
||||
|
||||
작업 전에 현재 브랜치와 워크트리 상태를 먼저 확인한다.
|
||||
|
||||
반드시 확인할 항목:
|
||||
|
||||
- 현재 브랜치
|
||||
- 원격 대비 ahead / behind 상태
|
||||
- 미푸시 커밋
|
||||
- 수정된 파일
|
||||
- 미추적 파일
|
||||
|
||||
금지:
|
||||
|
||||
- 로컬에서만 있는 상태를 기준 진실처럼 가정하기
|
||||
- 미정리 변경사항을 모른 채 새 작업을 덧붙이기
|
||||
|
||||
## Rule 4. DB Before Code Assumption
|
||||
|
||||
조직도, 멤버, 자리배치도, 권한 문제는 코드보다 DB 상태 영향을 먼저 의심한다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- dev DB와 prod DB가 다른데 코드 버그로 단정하지 않는다.
|
||||
- 공개용 기준 데이터가 필요한 검증은 먼저 동기화 상태를 확인한다.
|
||||
- DB 차이를 무시한 검증 결과를 신뢰하지 않는다.
|
||||
|
||||
## Rule 5. Dev / Prod Protocol Is Mandatory
|
||||
|
||||
`docs/DEV_PROD_DB_PROTOCOL.md` 의 규칙은 권고가 아니라 작업 기준이다.
|
||||
|
||||
핵심 원칙:
|
||||
|
||||
- 코드 선행은 `8081`
|
||||
- 데이터 정본은 `8080`
|
||||
- `8081` DB는 독립 정본이 아니라 검증용 복제본처럼 다룬다
|
||||
|
||||
조직도/자리배치도/멤버 검증 전에는 필요 시 아래를 먼저 수행한다.
|
||||
|
||||
- `./scripts/sync_prod_db_to_dev.sh minimal`
|
||||
|
||||
분석 화면까지 공개용 기준으로 맞출 필요가 있으면 아래를 사용한다.
|
||||
|
||||
- `./scripts/sync_prod_db_to_dev.sh full`
|
||||
|
||||
## Rule 6. Validation Before Completion
|
||||
|
||||
완료 기준은 "코드를 썼다"가 아니라 "실제 동작을 검증했다"이다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- 검증 없이 완료로 판단하지 않는다.
|
||||
- 감으로 확인하지 않고 체크리스트 기준으로 확인한다.
|
||||
- 회귀 가능성이 있는 수정은 관련 기능까지 같이 확인한다.
|
||||
|
||||
검증 기준 문서:
|
||||
|
||||
- `docs/REGRESSION_CHECKLIST.md`
|
||||
|
||||
## Rule 7. Seat Map Work Is High Risk
|
||||
|
||||
자리배치도 관련 작업은 항상 고위험 작업으로 취급한다.
|
||||
|
||||
작업 시 최소 확인 항목:
|
||||
|
||||
1. 관리자 DnD 배치 / 저장
|
||||
2. 조직도 상세의 seat preview
|
||||
3. 비관리자 seatmap 진입 / 표시
|
||||
|
||||
오피스가 여러 개면 아래 모두 확인한다.
|
||||
|
||||
- `기술개발센터`
|
||||
- `한맥빌딩 6층`
|
||||
- `한맥빌딩 7층`
|
||||
|
||||
기술개발센터만 보고 완료 처리하지 않는다.
|
||||
|
||||
## Rule 8. Auth / Schema / Sync Changes Are High Risk
|
||||
|
||||
아래 영역은 일반 기능 수정처럼 다루지 않는다.
|
||||
|
||||
- `auth.*`
|
||||
- `members`
|
||||
- `seat_maps`
|
||||
- `seat_slots`
|
||||
- `seat_positions`
|
||||
- 동기화 스크립트
|
||||
- 스키마 변경
|
||||
|
||||
이 작업은 반드시:
|
||||
|
||||
- 변경 이유 명시
|
||||
- 영향 범위 확인
|
||||
- 관련 검증 수행
|
||||
- 결과 기록
|
||||
|
||||
까지 포함해야 한다.
|
||||
|
||||
## Rule 9. Temporary Logic Must Be Tracked
|
||||
|
||||
mock, fallback, hotfix, 임시 우회 로직은 허용할 수 있다.
|
||||
하지만 반드시 추적 가능해야 한다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- 왜 임시인지 기록한다.
|
||||
- 제거 또는 정식화할 이슈를 연결한다.
|
||||
- 운영 기준 로직처럼 장기 방치하지 않는다.
|
||||
|
||||
## Rule 10. End-Of-Day Closing Record
|
||||
|
||||
작업 종료 시 아래를 반드시 남긴다.
|
||||
|
||||
- 무엇을 했는지
|
||||
- 무엇을 검증했는지
|
||||
- 무엇이 아직 남았는지
|
||||
- 다음에 어디서 이어야 하는지
|
||||
|
||||
남길 위치:
|
||||
|
||||
- Gitea 이슈 코멘트
|
||||
- 또는 체크포인트 문서
|
||||
|
||||
둘 다 가능하면 둘 다 남긴다.
|
||||
|
||||
## Rule 11. Commit And Push Need Explicit User Instruction
|
||||
|
||||
커밋과 푸시는 자동으로 하지 않는다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- 코드 수정, 문서 수정, 검증 작업은 커밋 없이 계속 진행할 수 있다.
|
||||
- `git commit` 은 사용자가 명시적으로 지시한 경우에만 수행한다.
|
||||
- `git push` 도 사용자가 명시적으로 지시한 경우에만 수행한다.
|
||||
- 작업 중간 상태는 워크트리에 남겨둘 수 있으며, 임의로 잘라서 자주 커밋하지 않는다.
|
||||
- 커밋이 필요하다고 판단되면 먼저 상태와 이유를 공유하고, 지시를 받은 뒤 진행한다.
|
||||
|
||||
## Rule 12. Promote 8081 To 8080 By Reviewed File Diff Only
|
||||
|
||||
`8081` 작업용에서 검증된 변경을 `8080` 공개용으로 가져갈 때는 전체 workspace 를 통째로 덮지 않는다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- 먼저 `8081` 작업용의 변경 파일 목록과 diff 를 확인한다.
|
||||
- 공개용에 필요한 파일만 선택해서 메인 workspace 로 반영한다.
|
||||
- 반영 후에는 메인 workspace 기준으로 최소 회귀 검증을 다시 수행한다.
|
||||
- `8081` DB 기준으로만 맞는 수정인지, `8080` 기준 데이터에서도 맞는지 다시 확인한다.
|
||||
- 검증이 끝나기 전에는 공개용 완료로 판단하지 않는다.
|
||||
|
||||
금지:
|
||||
|
||||
- `8081` 작업 디렉터리를 통째로 복사해서 `8080`에 덮어쓰기
|
||||
- diff 확인 없이 일괄 반영
|
||||
- `8081`에서 됐으니 `8080`도 같을 것이라고 가정하기
|
||||
|
||||
## Rule 13. 8081 Must Start From The Isolated Worktree
|
||||
|
||||
`8081` 작업은 항상 `.dev-worktree-8081` 기준으로 시작한다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- 디자인 작업도 예외가 아니다.
|
||||
- 허브/조직현황/프로젝트별 분석/사업관리대장 수정 전에 현재 실제 서빙 파일과 SSOT 파일을 먼저 확인한다.
|
||||
|
||||
디자인 작업 강제 우선순위:
|
||||
|
||||
1. `frontend/public/design-tokens.css`
|
||||
2. `frontend/public/design-patterns.css`
|
||||
3. `docs/architecture/DESIGN_SSOT.md`
|
||||
4. 그 다음 화면별 실제 서빙 파일
|
||||
|
||||
금지:
|
||||
|
||||
- reference/original 파일을 먼저 수정하기
|
||||
- 예전 파란톤/indigo/slate 계열을 새 기본값으로 다시 넣기
|
||||
- 토큰/패턴으로 해결 가능한 문제를 화면별 임시 하드코딩으로 처리하기
|
||||
|
||||
`8081` 작업용은 포트만 다른 복제 서버가 아니라, 코드 소스까지 분리된 전용 worktree여야 한다.
|
||||
|
||||
세부 규칙:
|
||||
|
||||
- `8081`은 항상 `.dev-worktree-8081`에서 띄운다.
|
||||
- 기동 전 `./scripts/prepare_dev_worktree.sh`를 먼저 실행한다.
|
||||
- 재부팅 후 빠른 기동은 `./scripts/start_8081.sh` 또는 `./scripts/start_local_dashboards.sh`를 사용한다.
|
||||
- `.env`와 로컬 전용 디자인 자산은 준비 스크립트가 복사한 것을 기준으로 사용한다.
|
||||
- 기동 후 `docker inspect mh-dashboard-organization-dev-backend-1`로 마운트 소스를 확인한다.
|
||||
|
||||
금지:
|
||||
|
||||
- 현재 메인 workspace를 직접 마운트한 상태로 `8081`을 띄우기
|
||||
- `8080`과 `8081`이 같은 `frontend/public`, `legacy/static`, `incoming-files`를 동시에 보게 두기
|
||||
- `8081`에서 보이던 디자인을 `8080` 공통 소스에 바로 덮어쓰기
|
||||
|
||||
## Daily Start Checklist
|
||||
|
||||
매일 첫 작업 시작 전 체크:
|
||||
|
||||
- 현재 브랜치 확인
|
||||
- 원격 대비 커밋 상태 확인
|
||||
- 열린 이슈 확인
|
||||
- `WORK_RULEBOOK.md` 확인
|
||||
- 최신 체크포인트 확인
|
||||
- 미추적 / 수정 파일 확인
|
||||
- 현재 작업은 커밋 없이 진행하고, 커밋/푸시는 지시받을 때만 한다는 규칙 확인
|
||||
- 오늘 작업이 코드 문제인지 DB 문제인지 먼저 구분
|
||||
- 공개용 기준 데이터 검증이 필요한지 판단
|
||||
|
||||
## Daily End Checklist
|
||||
|
||||
매일 작업 종료 전 체크:
|
||||
|
||||
- 오늘 변경 파일 정리
|
||||
- 검증 결과 정리
|
||||
- 미완료 항목 정리
|
||||
- 관련 이슈 코멘트 또는 문서 업데이트
|
||||
- 다음 시작 지점 명시
|
||||
|
||||
## One-Line Operating Principle
|
||||
|
||||
이 프로젝트의 작업 기준은 아래 한 줄로 요약한다.
|
||||
|
||||
- 상태를 먼저 확인하고, 완료 기능은 보호하며, DB와 검증을 무시하지 않고, 기록을 남기면서 작업한다.
|
||||
100
docs/architecture/8081_SERVING_MAP.md
Normal file
100
docs/architecture/8081_SERVING_MAP.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 8081 Serving Map
|
||||
|
||||
## Purpose
|
||||
|
||||
이 문서는 `8081` 작업용에서 어떤 URL이 어떤 파일을 실제로 읽는지 고정하기 위한 책임 맵이다.
|
||||
이번 1차 정리의 목표는 기능 변경이 아니라 `실제 서빙 파일`, `공통 기본 스타일`, `8081 전용 오버라이드`, `참고 원본 자산`의 경계를 분명히 하는 것이다.
|
||||
|
||||
## Runtime Entry Points
|
||||
|
||||
- 허브 엔트리: `/`
|
||||
- 파일: `frontend/public/index.html`
|
||||
- 허브 공통 스크립트:
|
||||
- 파일: `frontend/public/app.js`
|
||||
- 허브 공통 기본 스타일:
|
||||
- 파일: `frontend/public/styles.css`
|
||||
- 허브 8081 전용 디자인 오버라이드:
|
||||
- 파일: `frontend/public/styles-8081-design.css`
|
||||
|
||||
## Login Rules
|
||||
|
||||
- 로그인 화면 기본 구조와 스타일은 `8080` 공통 기준을 따른다.
|
||||
- 로그인 기본 스타일은 `frontend/public/styles.css`에서만 정의한다.
|
||||
- `frontend/public/styles-8081-design.css`에는 로그인 관련 셀렉터를 넣지 않는다.
|
||||
|
||||
## Legacy Organization
|
||||
|
||||
- URL: `/legacy/organization`
|
||||
- HTML 파일:
|
||||
- `DashBoard-organization.html`
|
||||
- 정적 자산:
|
||||
- `legacy/static/common.css`
|
||||
- `legacy/static/organization.css`
|
||||
- `legacy/static/organization.js`
|
||||
|
||||
## Integration Screens
|
||||
|
||||
- URL: `/integrations/payment`
|
||||
- 현재 실제 서빙 파일: `incoming-files/served/payment.html`
|
||||
- URL: `/integrations/mh`
|
||||
- 현재 실제 서빙 파일: `incoming-files/served/mh.html`
|
||||
|
||||
정리 원칙:
|
||||
|
||||
- `incoming-files` 아래에서는 `served/`를 실제 서빙 자산용으로 사용한다.
|
||||
- `reference/`는 원본 참고 파일, 복구 참고 파일, 비교용 자산만 둔다.
|
||||
- 1차 정리에서는 기존 실제 서빙 파일을 `served/`에 복사하고, backend 서빙 경로를 먼저 `served/`로 갱신한다.
|
||||
- 기존 루트 `incoming-files/payment.html`, `incoming-files/mh.html`는 안전한 비교/복구를 위해 당분간 남겨둔다.
|
||||
|
||||
## Seat Map
|
||||
|
||||
- 허브 화면 구성:
|
||||
- `frontend/public/index.html`
|
||||
- `frontend/public/app.js`
|
||||
- `frontend/public/styles.css`
|
||||
- `frontend/public/styles-8081-design.css`
|
||||
- API / viewer:
|
||||
- `backend/app/main.py`
|
||||
- `backend/app/db.py`
|
||||
- `backend/app/center_chair_viewer_template.html`
|
||||
|
||||
## Incoming Files Classification
|
||||
|
||||
### Served
|
||||
|
||||
- 실제 URL에서 직접 읽는 파일
|
||||
- 예:
|
||||
- `served/payment.html`
|
||||
- `served/mh.html`
|
||||
|
||||
### Reference
|
||||
|
||||
- 원본 HTML/CSS/XLSX/CSV
|
||||
- 복구 비교용 자산
|
||||
- 직접 서빙하지 않는 참고 파일
|
||||
- 필요 시 다음 차수에서 `reference/` 하위로 단계적 재배치한다.
|
||||
|
||||
예:
|
||||
|
||||
- `260320.html`
|
||||
- `sample style.css`
|
||||
- `opayment.html`
|
||||
- `omh.html`
|
||||
- `사업관리대장/*`
|
||||
- 원본 xlsx/csv
|
||||
|
||||
## Out Of Scope For Phase 1
|
||||
|
||||
- DB 스키마 의미 변경
|
||||
- 계산식 변경
|
||||
- 권한 로직 변경
|
||||
- 신규 기능 추가
|
||||
- backend 라우터 대분해
|
||||
|
||||
## Phase 1 Success Criteria
|
||||
|
||||
- 수정 대상 파일을 화면별로 즉시 찾을 수 있다.
|
||||
- 로그인은 `styles.css`만 본다.
|
||||
- 허브 8081 디자인은 `styles-8081-design.css`만 본다.
|
||||
- `/integrations/payment`, `/integrations/mh`의 실제 서빙 파일 위치가 문서와 코드에서 일치한다.
|
||||
- 기존 참고 자산을 지우지 않고도 실제 서빙 경로와 참고 경로를 구분할 수 있다.
|
||||
129
docs/architecture/DESIGN_SSOT.md
Normal file
129
docs/architecture/DESIGN_SSOT.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Design SSOT
|
||||
|
||||
## Source of truth
|
||||
|
||||
- Primary visual source: [incoming-files/sample style.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/incoming-files/sample%20style.css)
|
||||
- Runtime token file: [design-tokens.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-tokens.css)
|
||||
- Runtime pattern file: [design-patterns.css](/home/hyunho/projects/mh-dashboard-organization/.dev-worktree-8081/frontend/public/design-patterns.css)
|
||||
|
||||
`sample style.css` defines the intended MH visual language. `design-tokens.css` is the token-level SSOT, and `design-patterns.css` is the component-level SSOT that packages those tokens into reusable runtime patterns.
|
||||
|
||||
## Rules
|
||||
|
||||
- New UI must use `design-tokens.css` variables first.
|
||||
- New UI must use `design-patterns.css` patterns before adding page-local variants.
|
||||
- Direct hex values are exceptions, not defaults.
|
||||
- Page files may define layout and composition, but color, panel, border, radius, and shadow values must come from tokens.
|
||||
- Shared aliases in `legacy/static/common.css` and `frontend/public/styles.css` exist only to bridge older code to the SSOT.
|
||||
- Reference files under `incoming-files/*` are not visual authority. Runtime visuals must follow `design-tokens.css` and `design-patterns.css`.
|
||||
|
||||
## Fixed vs Flexible
|
||||
|
||||
SSOT is not a pixel-locked screenshot spec. It is a design rule system with two layers.
|
||||
|
||||
### Fixed rules
|
||||
|
||||
These should be treated as stable defaults across screens.
|
||||
|
||||
- Brand color family and accent family
|
||||
- Surface, border, text, and shadow tokens
|
||||
- Radius scale
|
||||
- Button, tab, input, panel, and card visual language
|
||||
- Typography tone and hierarchy
|
||||
- Background atmosphere and overall contrast direction
|
||||
|
||||
### Flexible rules
|
||||
|
||||
These must be interpreted per screen based on content density and interaction needs.
|
||||
|
||||
- KPI card width and number of columns
|
||||
- Sidebar/content split ratios
|
||||
- Table column widths
|
||||
- Search/filter placement
|
||||
- Card stacking and wrap behavior
|
||||
- Desktop/mobile breakpoint behavior
|
||||
|
||||
Example:
|
||||
|
||||
- Wrong SSOT: `KPI width is 100px`
|
||||
- Correct SSOT: `KPI cards use the shared panel, radius, spacing, and text hierarchy tokens, and their width adapts to content without collapsing readability`
|
||||
|
||||
## When SSOT does not define a component
|
||||
|
||||
If a screen needs a pattern that SSOT does not explicitly define yet, do not fall back to arbitrary legacy styling.
|
||||
|
||||
Use this order:
|
||||
|
||||
1. Reuse existing tokens and the nearest shared pattern
|
||||
2. Design the missing component in the same visual grammar
|
||||
3. If the pattern is likely to repeat, document and promote it into SSOT
|
||||
|
||||
This applies to examples such as:
|
||||
|
||||
- A table pattern that does not exist in the current SSOT
|
||||
- A KPI strip that needs a different density than the sample
|
||||
- A new modal layout for a data-heavy screen
|
||||
|
||||
## Candidate and deprecated styles
|
||||
|
||||
Not every style already visible in the product is automatically part of SSOT.
|
||||
|
||||
- `SSOT`
|
||||
- Approved and repeatable patterns
|
||||
- Token-backed visual rules
|
||||
- `candidate`
|
||||
- Screen-local styles that look usable but do not yet have a documented basis
|
||||
- Can be promoted later if they prove reusable
|
||||
- `deprecated`
|
||||
- Old blue/slate/indigo defaults
|
||||
- Temporary hardcoded fixes
|
||||
- Styles that conflict with the sample-based MH visual language
|
||||
|
||||
When a screen has a design with no clear basis, classify it as `candidate` first. Promote it only after it has been checked for reuse and consistency.
|
||||
|
||||
## Token groups
|
||||
|
||||
- Surface: `--ds-bg`, `--ds-panel`, `--ds-panel-soft`, `--ds-panel-strong`
|
||||
- Text: `--ds-ink`, `--ds-text-soft`, `--ds-text-muted`
|
||||
- Brand: `--ds-brand`, `--ds-brand-deep`, `--ds-brand-soft`, `--ds-accent`, `--ds-accent-soft`, `--ds-mint`
|
||||
- Borders and shadows: `--ds-line`, `--ds-line-soft`, `--ds-shadow-*`
|
||||
- Layout primitives: `--ds-radius-*`, `--ds-space-*`, `--ds-page-max-width`
|
||||
|
||||
## Promoted runtime patterns
|
||||
|
||||
These are now the official reusable patterns for current screens.
|
||||
|
||||
- Panels and heads: `.ds-panel`, `.ds-panel-head`
|
||||
- KPI cards: `.ds-kpi-card`, `.ds-kpi-people`, `.ds-kpi-inverse`
|
||||
- Filter surfaces and toggles: `.ds-filter-surface`, `.ds-filter-toggle`, `.ds-reset-button`
|
||||
- Tables: `.ds-table-head`, `.ds-table-head-row`, `.ds-table-row`, `.ds-axis-cell`, `.ds-axis-cell-idle`, `.ds-axis-cell-active`
|
||||
- Value emphasis: `.ds-project-cell`, `.ds-income`, `.ds-expense`, `.ds-subhead`, `.ds-empty`, `.ds-strong`, `.ds-muted`
|
||||
- Breakdown/detail UI: `.ds-progress-track*`, `.ds-mode-chip`, `.ds-name-chip`, `.ds-mini-table-*`, `.ds-group-title`
|
||||
- Position chips: `.ds-position-*` via `position-*` compatibility classes
|
||||
- Business ledger popup/detail blocks: `.popup-*`, `.inline-card`, `.project-head-*`, `.summary-*`, `.ledger-*`, `.badge`, `.project-link`
|
||||
- Organization modal forms/buttons: `.member-form-*`, `.modal-btn*`
|
||||
- Seatmap action visibility: `.seatmap-actions .ghost-button`
|
||||
|
||||
These patterns may still have compatibility selectors for existing screen classes, but they should now be treated as the official design layer.
|
||||
|
||||
## Migration order
|
||||
|
||||
1. Token file and common aliases
|
||||
2. Hub shell and shared controls
|
||||
3. Team/Personal analysis and Organization
|
||||
4. Project analysis
|
||||
5. Business ledger detail cleanup
|
||||
|
||||
## Implementation guidance
|
||||
|
||||
- Prefer tokenized ranges over hardcoded single values when layout depends on data volume
|
||||
- Prefer `design-patterns.css` component rules over one-off inline colors
|
||||
- If a new pattern is introduced during implementation, update this document once the pattern is stable
|
||||
- If a screen needs an exception, keep the exception local and explain why it cannot follow the shared pattern
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Adding new `#4f46e5`, `#4338ca`, `bg-slate-*`, `text-indigo-*` style defaults
|
||||
- Reintroducing separate page-level color systems
|
||||
- Hardcoding “quick fix” brand colors in JS templates when a token/class can carry the same intent
|
||||
- Letting reference/original files override runtime pattern files
|
||||
File diff suppressed because it is too large
Load Diff
730
frontend/public/design-patterns.css
Normal file
730
frontend/public/design-patterns.css
Normal file
@@ -0,0 +1,730 @@
|
||||
@import url("/design-tokens.css?v=20260401-01");
|
||||
|
||||
:root {
|
||||
--ds-hero-text: #f7f0e4;
|
||||
--ds-hero-border: rgba(242, 196, 132, 0.22);
|
||||
--ds-hero-surface: rgba(255, 255, 255, 0.08);
|
||||
--ds-hero-surface-strong: rgba(255, 255, 255, 0.1);
|
||||
--ds-hero-text-muted: rgba(255, 244, 230, 0.72);
|
||||
--ds-hero-text-soft: rgba(255, 244, 230, 0.56);
|
||||
--ds-hero-line: rgba(242, 196, 132, 0.18);
|
||||
--ds-danger-soft: rgba(169, 72, 50, 0.1);
|
||||
--ds-danger-line: rgba(169, 72, 50, 0.22);
|
||||
--ds-success-soft: rgba(47, 153, 115, 0.14);
|
||||
--ds-success-line: rgba(47, 153, 115, 0.24);
|
||||
--ds-brand-soft-surface: rgba(15, 58, 47, 0.1);
|
||||
--ds-brand-soft-line: rgba(15, 58, 47, 0.18);
|
||||
--ds-accent-soft-surface: rgba(242, 196, 132, 0.18);
|
||||
--ds-accent-soft-line: rgba(214, 138, 58, 0.24);
|
||||
}
|
||||
|
||||
.ds-panel,
|
||||
.payment-panel {
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
border: 1px solid var(--ds-line-soft);
|
||||
box-shadow: var(--ds-shadow-soft);
|
||||
}
|
||||
|
||||
.ds-panel-head,
|
||||
.payment-panel-head {
|
||||
background: rgba(255, 250, 243, 0.92);
|
||||
border-bottom: 1px solid var(--ds-line-soft);
|
||||
}
|
||||
|
||||
.ds-kpi-card,
|
||||
.payment-kpi-card {
|
||||
border: 1px solid var(--ds-line-soft);
|
||||
background: linear-gradient(180deg, rgba(255, 250, 243, 0.96) 0%, rgba(248, 242, 232, 0.96) 100%);
|
||||
box-shadow: var(--ds-shadow-soft);
|
||||
color: var(--ds-ink);
|
||||
}
|
||||
|
||||
.ds-kpi-inverse,
|
||||
.payment-kpi-inverse {
|
||||
color: #fffaf3;
|
||||
}
|
||||
|
||||
.ds-kpi-people,
|
||||
.payment-kpi-people {
|
||||
background: linear-gradient(135deg, var(--ds-brand) 0%, var(--ds-brand-soft) 100%);
|
||||
border-color: rgba(15, 58, 47, 0.2);
|
||||
}
|
||||
|
||||
.ds-subhead,
|
||||
.payment-subhead {
|
||||
color: var(--ds-text-muted);
|
||||
}
|
||||
|
||||
.ds-empty,
|
||||
.payment-empty {
|
||||
color: #9b937f;
|
||||
}
|
||||
|
||||
.ds-tooltip,
|
||||
.payment-tooltip {
|
||||
background: var(--ds-brand-deep);
|
||||
color: #fffaf3;
|
||||
}
|
||||
|
||||
.ds-filter-surface,
|
||||
.payment-filter-bar {
|
||||
background: rgba(246, 237, 221, 0.8);
|
||||
border: 1px solid var(--ds-line);
|
||||
}
|
||||
|
||||
.ds-filter-toggle,
|
||||
.payment-filter-toggle {
|
||||
background: var(--ds-brand);
|
||||
border-color: rgba(15, 58, 47, 0.28);
|
||||
color: #fffaf3;
|
||||
}
|
||||
|
||||
.ds-reset-button,
|
||||
.payment-reset-btn {
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
border: 1px solid var(--ds-line);
|
||||
color: var(--ds-text-muted);
|
||||
}
|
||||
|
||||
.ds-reset-button:hover,
|
||||
.payment-reset-btn:hover {
|
||||
color: var(--ds-brand-soft);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.ds-table-head,
|
||||
.payment-table-head {
|
||||
background: rgba(246, 237, 221, 0.82);
|
||||
}
|
||||
|
||||
.ds-table-head-row,
|
||||
.payment-table-head-row {
|
||||
color: var(--ds-brand-deep);
|
||||
border-bottom: 1px solid var(--ds-line);
|
||||
}
|
||||
|
||||
.ds-table-row,
|
||||
.payment-data-row {
|
||||
border-color: #f0e5d2;
|
||||
}
|
||||
|
||||
.ds-table-row:hover,
|
||||
.payment-data-row:hover {
|
||||
background: #f6eddd;
|
||||
}
|
||||
|
||||
.ds-axis-cell,
|
||||
.payment-axis-cell {
|
||||
border-right: 1px solid var(--ds-line-soft);
|
||||
}
|
||||
|
||||
.ds-axis-cell-idle,
|
||||
.payment-axis-cell-idle {
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
color: var(--ds-ink);
|
||||
}
|
||||
|
||||
.ds-axis-cell-idle:hover,
|
||||
.payment-axis-cell-idle:hover {
|
||||
background: rgba(234, 220, 196, 0.52);
|
||||
color: var(--ds-brand-deep);
|
||||
}
|
||||
|
||||
.ds-axis-cell-active,
|
||||
.payment-axis-cell-active {
|
||||
background: rgba(234, 220, 196, 0.78);
|
||||
color: var(--ds-brand-deep);
|
||||
}
|
||||
|
||||
.ds-project-cell,
|
||||
.payment-project-cell {
|
||||
color: var(--ds-brand-deep);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ds-project-cell:hover,
|
||||
.payment-project-cell:hover {
|
||||
background: #efe2ca;
|
||||
color: #214634;
|
||||
}
|
||||
|
||||
.ds-income,
|
||||
.payment-income {
|
||||
color: var(--ds-status-success);
|
||||
}
|
||||
|
||||
.ds-expense,
|
||||
.payment-expense {
|
||||
color: var(--ds-status-danger);
|
||||
}
|
||||
|
||||
.ds-progress-track,
|
||||
.payment-progress-track {
|
||||
background: rgba(217, 197, 168, 0.45);
|
||||
}
|
||||
|
||||
.ds-progress-track-grand,
|
||||
.payment-progress-track-grand {
|
||||
background: rgba(75, 135, 179, 0.24);
|
||||
}
|
||||
|
||||
.ds-progress-track-mid,
|
||||
.payment-progress-track-mid {
|
||||
background: rgba(214, 138, 58, 0.22);
|
||||
}
|
||||
|
||||
.ds-mode-chip,
|
||||
.payment-mode-chip {
|
||||
color: var(--ds-brand-soft);
|
||||
background: rgba(242, 196, 132, 0.22);
|
||||
border: 1px solid rgba(214, 138, 58, 0.28);
|
||||
}
|
||||
|
||||
.ds-name-chip,
|
||||
.payment-name-chip {
|
||||
background: rgba(246, 237, 221, 0.76);
|
||||
border: 1px solid var(--ds-line-soft);
|
||||
color: var(--ds-text-soft);
|
||||
}
|
||||
|
||||
.ds-divider-top,
|
||||
.payment-divider-top {
|
||||
border-top: 1px solid var(--ds-line-soft);
|
||||
}
|
||||
|
||||
.ds-divider-left,
|
||||
.payment-divider-left {
|
||||
border-left: 1px solid var(--ds-line-soft);
|
||||
}
|
||||
|
||||
.ds-divider-mark,
|
||||
.payment-divider-mark {
|
||||
color: rgba(183, 170, 147, 0.92);
|
||||
}
|
||||
|
||||
.ds-mini-table-shell,
|
||||
.payment-mini-table-shell {
|
||||
border: 1px solid var(--ds-line-soft);
|
||||
}
|
||||
|
||||
.ds-mini-table-head,
|
||||
.payment-mini-table-head {
|
||||
background: rgba(246, 237, 221, 0.68);
|
||||
color: var(--ds-text-muted);
|
||||
}
|
||||
|
||||
.ds-mini-table-row,
|
||||
.payment-mini-table-row {
|
||||
border-top: 1px solid rgba(217, 197, 168, 0.36);
|
||||
color: var(--ds-text-soft);
|
||||
}
|
||||
|
||||
.ds-group-title,
|
||||
.payment-group-title {
|
||||
background: var(--ds-brand);
|
||||
color: #fffaf3;
|
||||
}
|
||||
|
||||
.ds-strong,
|
||||
.payment-strong {
|
||||
color: var(--ds-ink);
|
||||
}
|
||||
|
||||
.ds-muted,
|
||||
.payment-muted {
|
||||
color: var(--ds-text-soft);
|
||||
}
|
||||
|
||||
.ds-accent-text,
|
||||
.payment-icon-accent {
|
||||
color: var(--ds-brand-soft);
|
||||
}
|
||||
|
||||
.ds-position-chip,
|
||||
.position-chip {
|
||||
background: rgba(246, 237, 221, 0.76);
|
||||
}
|
||||
|
||||
.ds-position-text,
|
||||
.position-text {
|
||||
color: var(--ds-text-soft);
|
||||
}
|
||||
|
||||
.ds-position-border,
|
||||
.position-border {
|
||||
border-color: rgba(217, 197, 168, 0.46);
|
||||
}
|
||||
|
||||
.ds-position-dot,
|
||||
.position-dot {
|
||||
box-shadow: 0 0 0 2px rgba(255, 250, 243, 0.9);
|
||||
}
|
||||
|
||||
.position-executive.position-chip { background: rgba(15, 58, 47, 0.1); }
|
||||
.position-executive.position-text { color: var(--ds-brand); }
|
||||
.position-executive.position-border { border-color: rgba(15, 58, 47, 0.22); }
|
||||
.position-executive.position-dot { background: var(--ds-brand); }
|
||||
|
||||
.position-principal.position-chip { background: rgba(26, 86, 69, 0.1); }
|
||||
.position-principal.position-text { color: var(--ds-brand-soft); }
|
||||
.position-principal.position-border { border-color: rgba(26, 86, 69, 0.22); }
|
||||
.position-principal.position-dot { background: var(--ds-brand-soft); }
|
||||
|
||||
.position-senior.position-chip { background: rgba(47, 153, 115, 0.12); }
|
||||
.position-senior.position-text { color: var(--ds-mint); }
|
||||
.position-senior.position-border { border-color: rgba(47, 153, 115, 0.26); }
|
||||
.position-senior.position-dot { background: var(--ds-mint); }
|
||||
|
||||
.position-associate.position-chip { background: rgba(75, 135, 179, 0.12); }
|
||||
.position-associate.position-text { color: var(--ds-info); }
|
||||
.position-associate.position-border { border-color: rgba(75, 135, 179, 0.22); }
|
||||
.position-associate.position-dot { background: var(--ds-info); }
|
||||
|
||||
.position-staff.position-chip { background: rgba(214, 138, 58, 0.12); }
|
||||
.position-staff.position-text { color: var(--ds-status-warning); }
|
||||
.position-staff.position-border { border-color: rgba(214, 138, 58, 0.24); }
|
||||
.position-staff.position-dot { background: var(--ds-status-warning); }
|
||||
|
||||
.position-member.position-chip { background: rgba(102, 117, 109, 0.12); }
|
||||
.position-member.position-text { color: var(--ds-text-soft); }
|
||||
.position-member.position-border { border-color: rgba(102, 117, 109, 0.24); }
|
||||
.position-member.position-dot { background: var(--ds-text-soft); }
|
||||
|
||||
.position-unset.position-chip { background: rgba(183, 170, 147, 0.18); }
|
||||
.position-unset.position-text { color: #8b7e69; }
|
||||
.position-unset.position-border { border-color: rgba(183, 170, 147, 0.3); }
|
||||
.position-unset.position-dot { background: #b7aa93; }
|
||||
|
||||
.popup-wrap {
|
||||
max-width: 1680px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popup-head {
|
||||
margin-bottom: 14px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(217, 197, 168, 0.62);
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, #fff8ee 0%, #f4e9d7 100%);
|
||||
box-shadow: 0 18px 36px rgba(15, 58, 47, 0.08);
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
color: var(--ds-ink);
|
||||
}
|
||||
|
||||
.popup-sub {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--ds-text-muted);
|
||||
}
|
||||
|
||||
.inline-panel {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.project-head-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.95fr) minmax(280px, 0.72fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.project-head-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-contact-stack {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-card,
|
||||
.ledger-block,
|
||||
.popup-wrap .ledger-block.collect {
|
||||
background: rgba(255, 250, 243, 0.98) !important;
|
||||
border: 1px solid rgba(217, 197, 168, 0.56) !important;
|
||||
border-radius: 24px !important;
|
||||
box-shadow: 0 16px 32px rgba(15, 58, 47, 0.08) !important;
|
||||
}
|
||||
|
||||
.inline-card {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.project-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 12px;
|
||||
}
|
||||
|
||||
.kv {
|
||||
padding: 12px 14px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, #fffdf8 0%, #f4e9d7 100%);
|
||||
border: 1px solid rgba(217, 197, 168, 0.46);
|
||||
}
|
||||
|
||||
.kvk,
|
||||
.summary-label {
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #8a6b3d;
|
||||
}
|
||||
|
||||
.kvv {
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
color: var(--ds-ink);
|
||||
line-height: 1.35;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.summary-note {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: var(--ds-text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, #fffdf8 0%, #f4e9d7 100%);
|
||||
border: 1px solid rgba(217, 197, 168, 0.46);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.summary-card.receivable {
|
||||
background: linear-gradient(180deg, var(--ds-danger-soft) 0%, rgba(255, 248, 238, 0.98) 100%);
|
||||
border-color: var(--ds-danger-line);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
color: var(--ds-ink);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.summary-card.receivable .summary-value {
|
||||
color: var(--ds-status-danger);
|
||||
}
|
||||
|
||||
.project-progress {
|
||||
margin-top: 10px;
|
||||
height: 12px;
|
||||
border-radius: var(--ds-radius-pill);
|
||||
overflow: hidden;
|
||||
background: rgba(217, 197, 168, 0.48);
|
||||
box-shadow: inset 0 1px 2px rgba(15, 58, 47, 0.08);
|
||||
}
|
||||
|
||||
.progress .bar {
|
||||
height: 100%;
|
||||
border-radius: var(--ds-radius-pill);
|
||||
background: linear-gradient(90deg, var(--ds-brand-soft) 0%, var(--ds-mint) 100%);
|
||||
box-shadow: 0 8px 18px rgba(47, 153, 115, 0.18);
|
||||
}
|
||||
|
||||
.ledger-stack {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ledger-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 18px 18px 14px;
|
||||
border-bottom: 1px solid rgba(217, 197, 168, 0.38) !important;
|
||||
background: linear-gradient(180deg, #fffdf8 0%, #f4e9d7 100%) !important;
|
||||
}
|
||||
|
||||
.ledger-head-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ledger-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(180deg, #fff8ee 0%, #f2dec0 100%) !important;
|
||||
color: var(--ds-accent-strong) !important;
|
||||
font-weight: 900;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.ledger-name {
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
color: var(--ds-ink) !important;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.ledger-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: var(--ds-text-muted) !important;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ledger-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--ds-radius-pill);
|
||||
background: var(--ds-brand-soft-surface) !important;
|
||||
border: 1px solid var(--ds-brand-soft-line) !important;
|
||||
color: var(--ds-brand-soft) !important;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ledger-table-wrap {
|
||||
padding: 0 16px 16px;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ledger-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ledger-table thead th {
|
||||
padding: 12px 10px;
|
||||
background: var(--ds-brand) !important;
|
||||
color: #fff5e6 !important;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
text-align: left;
|
||||
border-right: 1px solid rgba(242, 196, 132, 0.18) !important;
|
||||
}
|
||||
|
||||
.ledger-table thead th:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.ledger-table tbody td {
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid rgba(217, 197, 168, 0.34) !important;
|
||||
vertical-align: top;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--ds-ink) !important;
|
||||
background: rgba(255, 250, 243, 0.72) !important;
|
||||
}
|
||||
|
||||
.ledger-table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ledger-main {
|
||||
display: block;
|
||||
color: var(--ds-ink) !important;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.ledger-muted,
|
||||
.ledger-note {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--ds-text-muted) !important;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ledger-amount {
|
||||
font-weight: 900;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--ds-radius-pill);
|
||||
border: 1px solid rgba(217, 197, 168, 0.5);
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
color: #17392f;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.badge.badge-baron {
|
||||
background: var(--ds-brand-soft-surface);
|
||||
border-color: var(--ds-brand-soft-line);
|
||||
color: var(--ds-brand-soft);
|
||||
}
|
||||
|
||||
.badge.badge-family {
|
||||
background: var(--ds-accent-soft-surface);
|
||||
border-color: var(--ds-accent-soft-line);
|
||||
color: var(--ds-status-warning);
|
||||
}
|
||||
|
||||
.badge.ok {
|
||||
background: var(--ds-success-soft);
|
||||
border-color: var(--ds-success-line);
|
||||
color: var(--ds-brand-soft);
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #17392f;
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-link:hover {
|
||||
color: #0f6a55;
|
||||
}
|
||||
|
||||
.member-form-label {
|
||||
color: var(--ds-text-soft);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.member-form-input,
|
||||
.member-form-select,
|
||||
.member-form-time {
|
||||
border: 1px solid var(--ds-line-soft);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 250, 243, 0.92);
|
||||
color: var(--ds-ink);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.member-form-input:focus,
|
||||
.member-form-select:focus,
|
||||
.member-form-time:focus {
|
||||
border-color: rgba(47, 153, 115, 0.45);
|
||||
box-shadow: 0 0 0 4px rgba(47, 153, 115, 0.1);
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
min-height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--ds-radius-pill);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.modal-btn-cancel {
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
border-color: var(--ds-line);
|
||||
color: var(--ds-text-soft);
|
||||
}
|
||||
|
||||
.modal-btn-save {
|
||||
background: var(--ds-brand-soft);
|
||||
border-color: rgba(15, 58, 47, 0.22);
|
||||
color: #fffaf3;
|
||||
}
|
||||
|
||||
.modal-btn-delete {
|
||||
background: rgba(169, 72, 50, 0.12);
|
||||
border-color: rgba(169, 72, 50, 0.24);
|
||||
color: var(--ds-status-danger);
|
||||
}
|
||||
|
||||
.modal-btn-close {
|
||||
background: rgba(242, 196, 132, 0.18);
|
||||
border-color: rgba(214, 138, 58, 0.24);
|
||||
color: var(--ds-status-warning);
|
||||
}
|
||||
|
||||
.seatmap-actions .ghost-button {
|
||||
min-height: 40px;
|
||||
padding: 0 16px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: var(--ds-radius-pill);
|
||||
font-size: 12px;
|
||||
letter-spacing: -0.01em;
|
||||
box-shadow: var(--ds-shadow-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.project-head-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.project-meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.popup-wrap {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ledger-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ledger-pill {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ledger-table-wrap {
|
||||
padding: 0 10px 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
60
frontend/public/design-tokens.css
Normal file
60
frontend/public/design-tokens.css
Normal file
@@ -0,0 +1,60 @@
|
||||
:root {
|
||||
--ds-font-sans: "Pretendard", "Malgun Gothic", sans-serif;
|
||||
|
||||
--ds-bg: #f1eadf;
|
||||
--ds-bg-soft: #f4e9d7;
|
||||
--ds-bg-gradient:
|
||||
radial-gradient(circle at top left, rgba(214, 138, 58, 0.18), transparent 24%),
|
||||
radial-gradient(circle at top right, rgba(47, 153, 115, 0.12), transparent 22%),
|
||||
linear-gradient(180deg, #f6efe6 0%, #f1eadf 100%);
|
||||
|
||||
--ds-panel: #fffaf3;
|
||||
--ds-panel-soft: rgba(255, 250, 243, 0.9);
|
||||
--ds-panel-strong: #eadcc4;
|
||||
|
||||
--ds-ink: #10251d;
|
||||
--ds-text-soft: #425148;
|
||||
--ds-text-muted: #66756d;
|
||||
|
||||
--ds-line: #d9c5a8;
|
||||
--ds-line-soft: rgba(217, 197, 168, 0.45);
|
||||
|
||||
--ds-brand: #0f3a2f;
|
||||
--ds-brand-deep: #0a2a22;
|
||||
--ds-brand-soft: #1a5645;
|
||||
--ds-accent: #d68a3a;
|
||||
--ds-accent-soft: #f2c484;
|
||||
--ds-accent-strong: #b66e22;
|
||||
--ds-mint: #2f9973;
|
||||
--ds-info: #4b87b3;
|
||||
|
||||
--ds-status-success: #2f6b52;
|
||||
--ds-status-warning: #9a6422;
|
||||
--ds-status-danger: #a94832;
|
||||
|
||||
--ds-surface-tint: rgba(255, 255, 255, 0.72);
|
||||
--ds-surface-tint-strong: rgba(255, 255, 255, 0.88);
|
||||
--ds-glass-dark: rgba(20, 45, 37, 0.34);
|
||||
--ds-glass-dark-soft: rgba(16, 37, 29, 0.16);
|
||||
--ds-glass-line: rgba(255, 255, 255, 0.14);
|
||||
|
||||
--ds-shadow-soft: 0 10px 24px rgba(15, 58, 47, 0.08);
|
||||
--ds-shadow-card: 0 22px 54px rgba(15, 58, 47, 0.12);
|
||||
--ds-shadow-float: 0 18px 36px rgba(15, 58, 47, 0.16);
|
||||
--ds-shadow-hero: 0 28px 70px rgba(15, 58, 47, 0.22);
|
||||
|
||||
--ds-radius-sm: 8px;
|
||||
--ds-radius-md: 12px;
|
||||
--ds-radius-lg: 18px;
|
||||
--ds-radius-xl: 24px;
|
||||
--ds-radius-pill: 999px;
|
||||
|
||||
--ds-space-1: 4px;
|
||||
--ds-space-2: 8px;
|
||||
--ds-space-3: 12px;
|
||||
--ds-space-4: 16px;
|
||||
--ds-space-5: 20px;
|
||||
--ds-space-6: 24px;
|
||||
|
||||
--ds-page-max-width: 2000px;
|
||||
}
|
||||
@@ -3,12 +3,22 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MH 조직현황 대시보드</title>
|
||||
<title>MH 대시보드-공개용</title>
|
||||
<script>
|
||||
document.title = window.location.port === '8081'
|
||||
? 'MH 대시보드-작업용'
|
||||
: 'MH 대시보드-공개용';
|
||||
</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/design-tokens.css?v=20260401-01">
|
||||
<link rel="stylesheet" href="/design-patterns.css?v=20260401-01">
|
||||
<link rel="stylesheet" href="/legacy/static/common.css">
|
||||
<link rel="stylesheet" href="/styles.css?v=20260325-11">
|
||||
<!-- Keep login and common hub defaults aligned with 8080. -->
|
||||
<link rel="stylesheet" href="/styles.css?v=20260330-01">
|
||||
<!-- 8081-only hub overrides must not restyle the login screen. -->
|
||||
<link rel="stylesheet" href="/styles-8081-design.css?v=20260401-01">
|
||||
</head>
|
||||
<body>
|
||||
<section id="login-panel" class="login-screen">
|
||||
@@ -37,93 +47,174 @@
|
||||
|
||||
<section id="dashboard-panel" class="dashboard-shell hidden">
|
||||
<header class="dashboard-header">
|
||||
<div class="brand-block">
|
||||
<p class="eyebrow">MH Dashboard</p>
|
||||
<h2 id="current-view-title">조직 현황</h2>
|
||||
<div class="header-left">
|
||||
<div class="brand-block">
|
||||
<p class="eyebrow">MH Dashboard</p>
|
||||
<h2 id="current-view-title">조직 현황</h2>
|
||||
</div>
|
||||
|
||||
<div id="global-date-controls" class="header-date-controls hidden">
|
||||
<span class="header-date-label">기간</span>
|
||||
<label class="header-date-field">
|
||||
<input id="global-start-date" type="date" aria-label="시작일">
|
||||
</label>
|
||||
<span class="header-date-sep">~</span>
|
||||
<label class="header-date-field">
|
||||
<input id="global-end-date" type="date" aria-label="종료일">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="organization-history-controls" class="header-date-controls hidden">
|
||||
<span class="header-date-label">조직 기준월</span>
|
||||
<label class="header-date-field">
|
||||
<select id="organization-month-select" aria-label="조직 기준월"></select>
|
||||
</label>
|
||||
<button id="organization-compare-btn" class="ghost-button ghost-button-soft hidden" type="button">조직도 변경사항 확인</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<button class="nav-pill" type="button" data-view="ledger">사업관리대장</button>
|
||||
<button class="nav-pill" type="button" data-view="project">프로젝트별 분석</button>
|
||||
<button class="nav-pill" type="button" data-view="team">팀/개인별 분석</button>
|
||||
<button class="nav-pill active" type="button" data-view="organization">조직 현황</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-center">
|
||||
<button class="nav-pill" type="button" data-view="ledger">사업관리대장</button>
|
||||
<button class="nav-pill" type="button" data-view="project">프로젝트별 분석</button>
|
||||
<button class="nav-pill" type="button" data-view="team">팀/개인별 분석</button>
|
||||
<button class="nav-pill active" type="button" data-view="organization">조직 현황</button>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<button id="user-badge" class="ghost-button ghost-button-soft user-chip" type="button"></button>
|
||||
<div id="user-popover" class="user-popover hidden"></div>
|
||||
<button id="logout-btn" class="ghost-button icon-button" type="button" title="로그아웃" aria-label="로그아웃">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button id="user-badge" class="ghost-button ghost-button-soft user-chip" type="button"></button>
|
||||
<div id="user-popover" class="user-popover hidden"></div>
|
||||
<button id="logout-btn" class="ghost-button icon-button" type="button" title="로그아웃" aria-label="로그아웃">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="dashboard-main">
|
||||
<section id="organization-stage" class="main-stage">
|
||||
<section id="ledger-stage" class="main-stage" hidden>
|
||||
<div class="stage-frame">
|
||||
<iframe id="organization-frame" src="/legacy/organization?v=20260325-11" data-src="/legacy/organization?v=20260325-11" title="조직도 메인 화면"></iframe>
|
||||
<iframe id="ledger-frame" src="/integrations/ledger?v=20260401-02" data-src="/integrations/ledger?v=20260401-02" title="사업관리대장 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="seatmap-stage" class="main-stage" hidden>
|
||||
<section id="organization-stage" class="main-stage">
|
||||
<div class="stage-frame">
|
||||
<!-- Legacy organization keeps its own CSS/JS responsibility under /legacy/static. -->
|
||||
<iframe id="organization-frame" src="/legacy/organization?v=20260330-02" data-src="/legacy/organization?v=20260330-02" title="조직도 메인 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="project-stage" class="main-stage" hidden>
|
||||
<div class="stage-frame">
|
||||
<!-- Integration HTML is served from incoming-files/served/payment.html. -->
|
||||
<iframe id="project-frame" src="/integrations/payment" data-src="/integrations/payment" title="프로젝트별 분석 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="team-stage" class="main-stage" hidden>
|
||||
<div class="stage-frame">
|
||||
<!-- Integration HTML is served from incoming-files/served/mh.html. -->
|
||||
<iframe id="team-frame" src="/integrations/mh" data-src="/integrations/mh" title="팀/개인별 분석 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="seatmap-admin-stage" class="main-stage" hidden>
|
||||
<div class="seatmap-layout">
|
||||
<div class="seatmap-topbar">
|
||||
<div>
|
||||
<p class="eyebrow">Seat Layout</p>
|
||||
<h3 id="seatmap-name">자리배치도</h3>
|
||||
<h3 id="seatmap-admin-name">자리배치도</h3>
|
||||
</div>
|
||||
<div class="seatmap-actions">
|
||||
<button id="seatmap-save-btn" class="ghost-button" type="button" hidden disabled>저장</button>
|
||||
<button id="seatmap-cancel-btn" class="ghost-button ghost-button-soft" type="button" hidden>취소</button>
|
||||
<div id="seatmap-admin-office-tabs" class="seatmap-office-tabs"></div>
|
||||
<div class="seatmap-actions" id="seatmap-admin-actions">
|
||||
<button id="seatmap-admin-save-btn" class="ghost-button" type="button" hidden disabled>저장</button>
|
||||
<button id="seatmap-admin-exit-btn" class="ghost-button ghost-button-soft" type="button" hidden>나가기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="seatmap-status" class="seatmap-status" role="status"></p>
|
||||
<p id="seatmap-admin-status" class="seatmap-status" role="status"></p>
|
||||
|
||||
<div class="seatmap-content">
|
||||
<div class="seatmap-board-panel">
|
||||
<div id="seatmap-empty" class="seatmap-empty hidden"></div>
|
||||
<div id="seatmap-board-wrap" class="seatmap-board-wrap hidden">
|
||||
<div id="seatmap-board" class="seatmap-board"></div>
|
||||
<div id="seatmap-admin-empty" class="seatmap-empty hidden"></div>
|
||||
<div id="seatmap-admin-board-wrap" class="seatmap-board-wrap hidden">
|
||||
<div id="seatmap-admin-board" class="seatmap-board"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="seatmap-sidebar">
|
||||
<section id="seatmap-settings-panel" class="seatmap-panel hidden">
|
||||
<section id="seatmap-admin-settings-panel" class="seatmap-panel hidden">
|
||||
<div class="seatmap-panel-head">
|
||||
<h4>도면 설정</h4>
|
||||
<p>현재는 기술개발센터 고정 도면을 사용합니다.</p>
|
||||
</div>
|
||||
<form id="seatmap-settings-form" class="seatmap-form">
|
||||
<form id="seatmap-admin-settings-form" class="seatmap-form">
|
||||
<label>
|
||||
<span>도면 이름</span>
|
||||
<input id="seatmap-form-name" name="name" type="text" placeholder="예: 기술개발센터" required>
|
||||
<input id="seatmap-admin-form-name" name="name" type="text" placeholder="예: 기술개발센터" required>
|
||||
</label>
|
||||
<div>
|
||||
<span>DXF 파일</span>
|
||||
<label class="seatmap-file-input" for="seatmap-form-image">
|
||||
<input id="seatmap-form-image" name="image" type="file" accept=".dxf" required>
|
||||
<label class="seatmap-file-input" for="seatmap-admin-form-image">
|
||||
<input id="seatmap-admin-form-image" name="image" type="file" accept=".dxf" required>
|
||||
<span class="seatmap-file-button">DXF 선택</span>
|
||||
<strong id="seatmap-file-name" class="seatmap-file-name">선택된 파일 없음</strong>
|
||||
<strong id="seatmap-admin-file-name" class="seatmap-file-name">선택된 파일 없음</strong>
|
||||
</label>
|
||||
</div>
|
||||
<button id="seatmap-settings-submit" type="submit">DXF 업로드</button>
|
||||
<button id="seatmap-admin-settings-submit" type="submit">DXF 업로드</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="seatmap-panel">
|
||||
<div class="seatmap-panel-head">
|
||||
<h4>미배치 인원</h4>
|
||||
<p>이름을 검색하고 자리배치도에 바로 드래그하세요.</p>
|
||||
<h4 id="seatmap-admin-sidebar-title">전체 인원</h4>
|
||||
<p id="seatmap-admin-sidebar-desc">미배치 인원은 상단, 배치 완료 인원은 하단에 표시됩니다.</p>
|
||||
</div>
|
||||
<label class="seatmap-search">
|
||||
<span class="hidden">구성원 검색</span>
|
||||
<input id="seatmap-search" type="search" placeholder="이름 또는 부서 검색">
|
||||
<input id="seatmap-admin-search" type="search" placeholder="이름 또는 부서 검색">
|
||||
</label>
|
||||
<div id="seatmap-unassigned" class="seatmap-member-list"></div>
|
||||
<div id="seatmap-admin-unassigned" class="seatmap-member-list"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="seatmap-readonly-stage" class="main-stage" hidden>
|
||||
<div class="seatmap-layout">
|
||||
<div class="seatmap-topbar">
|
||||
<div>
|
||||
<p class="eyebrow">Seat Layout</p>
|
||||
<h3 id="seatmap-readonly-name">자리배치도</h3>
|
||||
</div>
|
||||
<div id="seatmap-readonly-office-tabs" class="seatmap-office-tabs"></div>
|
||||
<div class="seatmap-actions" id="seatmap-readonly-actions">
|
||||
<button id="seatmap-readonly-exit-btn" class="ghost-button ghost-button-soft" type="button" hidden>나가기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="seatmap-readonly-status" class="seatmap-status" role="status"></p>
|
||||
|
||||
<div class="seatmap-content">
|
||||
<div class="seatmap-board-panel">
|
||||
<div id="seatmap-readonly-empty" class="seatmap-empty hidden"></div>
|
||||
<div id="seatmap-readonly-board-wrap" class="seatmap-board-wrap hidden">
|
||||
<div id="seatmap-readonly-board" class="seatmap-board"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="seatmap-sidebar">
|
||||
<section class="seatmap-panel">
|
||||
<div class="seatmap-panel-head">
|
||||
<h4 id="seatmap-readonly-sidebar-title">배치 인원 검색</h4>
|
||||
<p id="seatmap-readonly-sidebar-desc">이름이나 부서를 검색하고 클릭하면 해당 좌석으로 바로 확대 이동합니다.</p>
|
||||
</div>
|
||||
<label class="seatmap-search">
|
||||
<span class="hidden">구성원 검색</span>
|
||||
<input id="seatmap-readonly-search" type="search" placeholder="이름 또는 부서 검색">
|
||||
</label>
|
||||
<div id="seatmap-readonly-unassigned" class="seatmap-member-list"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -135,6 +226,6 @@
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<script src="/app.js?v=20260325-11"></script>
|
||||
<script src="/app.js?v=20260401-02"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
100
frontend/public/styles-8081-design.css
Normal file
100
frontend/public/styles-8081-design.css
Normal file
@@ -0,0 +1,100 @@
|
||||
.dashboard-header {
|
||||
min-height: 68px;
|
||||
background:
|
||||
radial-gradient(circle at 12% 18%, rgba(242, 196, 132, 0.16), transparent 24%),
|
||||
linear-gradient(145deg, rgba(10, 42, 34, 0.96) 0%, rgba(15, 58, 47, 0.96) 52%, rgba(26, 86, 69, 0.96) 100%);
|
||||
color: #f7f0e4;
|
||||
border-bottom: 1px solid rgba(242, 196, 132, 0.22);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: var(--ds-shadow-float);
|
||||
}
|
||||
|
||||
.dashboard-header .eyebrow {
|
||||
color: rgba(242, 196, 132, 0.94);
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
color: #fff7ea;
|
||||
}
|
||||
|
||||
.nav-pill {
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(242, 196, 132, 0.28);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 244, 230, 0.78);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.nav-pill.active {
|
||||
background: linear-gradient(180deg, rgba(255, 253, 248, 0.98), rgba(245, 235, 221, 0.94));
|
||||
border-color: rgba(242, 196, 132, 0.34);
|
||||
color: var(--ds-ink);
|
||||
box-shadow: var(--ds-shadow-float);
|
||||
}
|
||||
|
||||
.nav-pill.muted {
|
||||
color: rgba(255, 244, 230, 0.48);
|
||||
}
|
||||
|
||||
.nav-pill:hover {
|
||||
color: #fff7ea;
|
||||
border-color: rgba(242, 196, 132, 0.48);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
border-left: 1px solid rgba(242, 196, 132, 0.2);
|
||||
}
|
||||
|
||||
.header-date-label {
|
||||
color: rgba(255, 244, 230, 0.72);
|
||||
}
|
||||
|
||||
.header-date-field {
|
||||
border: 1px solid rgba(242, 196, 132, 0.22);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.header-date-field input,
|
||||
.header-date-field select {
|
||||
color: #fff7ea;
|
||||
}
|
||||
|
||||
.header-date-sep {
|
||||
color: rgba(255, 244, 230, 0.56);
|
||||
}
|
||||
|
||||
.ghost-button {
|
||||
border: 1px solid rgba(242, 196, 132, 0.22);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff7ea;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background: rgba(242, 196, 132, 0.14);
|
||||
border-color: rgba(242, 196, 132, 0.32);
|
||||
color: #fff7ea;
|
||||
}
|
||||
|
||||
.ghost-button-soft {
|
||||
background: rgba(239, 228, 208, 0.92);
|
||||
}
|
||||
|
||||
.seatmap-status[data-tone="error"] {
|
||||
color: var(--ds-status-danger);
|
||||
}
|
||||
|
||||
.seatmap-status[data-tone="success"] {
|
||||
color: var(--ds-status-success);
|
||||
}
|
||||
|
||||
.seatmap-board-wrap,
|
||||
.seatmap-dxf-canvas {
|
||||
background: var(--ds-panel);
|
||||
}
|
||||
@@ -1,3 +1,46 @@
|
||||
:root {
|
||||
--color-bg: var(--ds-bg);
|
||||
--color-surface: var(--ds-panel);
|
||||
--color-surface-soft: var(--ds-panel-soft);
|
||||
--color-surface-strong: var(--ds-panel-strong);
|
||||
--color-text: var(--ds-ink);
|
||||
--color-text-soft: var(--ds-text-soft);
|
||||
--color-text-muted: var(--ds-text-muted);
|
||||
--color-border: var(--ds-line);
|
||||
--color-border-soft: var(--ds-line-soft);
|
||||
--color-brand: var(--ds-brand);
|
||||
--color-brand-deep: var(--ds-brand-deep);
|
||||
--color-brand-soft: var(--ds-brand-soft);
|
||||
--color-accent: var(--ds-accent);
|
||||
--color-accent-soft: var(--ds-accent-soft);
|
||||
--color-success: var(--ds-status-success);
|
||||
--color-danger: var(--ds-status-danger);
|
||||
--radius-sm: var(--ds-radius-sm);
|
||||
--radius-md: var(--ds-radius-md);
|
||||
--radius-lg: var(--ds-radius-lg);
|
||||
--radius-xl: var(--ds-radius-xl);
|
||||
--radius-pill: var(--ds-radius-pill);
|
||||
--shadow-soft: var(--ds-shadow-soft);
|
||||
--shadow-card: var(--ds-shadow-card);
|
||||
--shadow-float: var(--ds-shadow-float);
|
||||
}
|
||||
|
||||
.dashboard-shell,
|
||||
.dashboard-main,
|
||||
.main-stage,
|
||||
.seatmap-layout,
|
||||
.seatmap-content,
|
||||
.seatmap-board-panel,
|
||||
.seatmap-sidebar {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -15,7 +58,7 @@
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(15, 23, 42, 0.42), rgba(30, 41, 59, 0.18)),
|
||||
linear-gradient(135deg, rgba(10, 42, 34, 0.42), rgba(26, 86, 69, 0.18)),
|
||||
url("https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1800&q=80")
|
||||
center center / cover no-repeat;
|
||||
}
|
||||
@@ -38,10 +81,10 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 0.7fr;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border: 1px solid var(--ds-glass-line);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(71, 85, 105, 0.34);
|
||||
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.24);
|
||||
background: var(--ds-glass-dark);
|
||||
box-shadow: var(--ds-shadow-hero);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
@@ -52,8 +95,8 @@
|
||||
padding: 30px 30px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(15, 23, 42, 0.08), rgba(255, 255, 255, 0.02)),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(15, 23, 42, 0.08));
|
||||
linear-gradient(90deg, rgba(10, 42, 34, 0.08), rgba(255, 255, 255, 0.02)),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(10, 42, 34, 0.08));
|
||||
}
|
||||
|
||||
.login-brand .eyebrow {
|
||||
@@ -67,7 +110,7 @@
|
||||
font-size: clamp(1.7rem, 3.2vw, 2.5rem);
|
||||
line-height: 0.96;
|
||||
letter-spacing: -0.04em;
|
||||
color: #f8fafc;
|
||||
color: #f7f0e4;
|
||||
}
|
||||
|
||||
.login-form-wrap {
|
||||
@@ -75,7 +118,7 @@
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 10px;
|
||||
background: rgba(15, 23, 42, 0.12);
|
||||
background: var(--ds-glass-dark-soft);
|
||||
}
|
||||
|
||||
.login-card label {
|
||||
@@ -124,8 +167,8 @@
|
||||
margin-top: 2px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
background: rgba(31, 41, 55, 0.82);
|
||||
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.22);
|
||||
background: rgba(10, 42, 34, 0.82);
|
||||
box-shadow: var(--shadow-float);
|
||||
min-height: 34px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
@@ -143,16 +186,17 @@
|
||||
}
|
||||
|
||||
.dashboard-shell {
|
||||
min-height: 100vh;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
min-height: 68px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
background: rgba(255, 250, 243, 0.94);
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid #d7dee8;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -163,12 +207,35 @@
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right,
|
||||
.brand-block,
|
||||
.header-actions {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-block {
|
||||
flex: 0 0 auto;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.dashboard-header .eyebrow {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 2px;
|
||||
@@ -182,14 +249,13 @@
|
||||
}
|
||||
|
||||
.header-center {
|
||||
margin-left: auto;
|
||||
margin-right: 48px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
gap: 24px;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.nav-pill {
|
||||
@@ -202,7 +268,7 @@
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -216,7 +282,7 @@
|
||||
}
|
||||
|
||||
.nav-pill.muted {
|
||||
color: #94a3b8;
|
||||
color: rgba(102, 117, 109, 0.64);
|
||||
}
|
||||
|
||||
.nav-pill:hover {
|
||||
@@ -229,12 +295,65 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.header-date-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 0;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-date-label {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.header-date-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.header-date-field input {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header-date-field select {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.header-date-sep {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ghost-button {
|
||||
min-height: 34px;
|
||||
border: 1px solid #dbe2ea;
|
||||
background: #fff;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
@@ -250,12 +369,12 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
background: var(--color-surface-soft);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #cbd5e1;
|
||||
background: var(--ds-bg-soft);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
@@ -271,7 +390,7 @@
|
||||
}
|
||||
|
||||
.ghost-button-soft {
|
||||
background: #f8fafc;
|
||||
background: var(--color-surface-soft);
|
||||
}
|
||||
|
||||
.user-chip {
|
||||
@@ -289,8 +408,8 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
background: var(--color-surface-strong);
|
||||
color: var(--color-text-soft);
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
flex: 0 0 auto;
|
||||
@@ -329,10 +448,10 @@
|
||||
right: 0;
|
||||
min-width: 220px;
|
||||
padding: 14px;
|
||||
border: 1px solid #dbe2ea;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.14);
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
box-shadow: var(--shadow-float);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 30;
|
||||
}
|
||||
@@ -348,7 +467,7 @@
|
||||
}
|
||||
|
||||
.user-popover-row + .user-popover-row {
|
||||
border-top: 1px solid #eef2f7;
|
||||
border-top: 1px solid rgba(217, 197, 168, 0.4);
|
||||
}
|
||||
|
||||
.user-popover-label {
|
||||
@@ -362,7 +481,7 @@
|
||||
min-height: 38px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #0f172a;
|
||||
background: var(--color-brand);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
@@ -371,14 +490,16 @@
|
||||
|
||||
.dashboard-main {
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 68px);
|
||||
height: calc(100dvh - 68px);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-stage {
|
||||
height: calc(100vh - 68px);
|
||||
height: calc(100dvh - 68px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stage-frame {
|
||||
@@ -391,7 +512,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.stage-empty {
|
||||
@@ -407,18 +528,43 @@
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(241, 245, 249, 0.92)),
|
||||
radial-gradient(circle at top left, rgba(14, 165, 233, 0.1), transparent 32%);
|
||||
overflow: hidden;
|
||||
background: var(--ds-bg-gradient);
|
||||
}
|
||||
|
||||
.seatmap-topbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1fr) auto minmax(180px, 1fr);
|
||||
align-items: end;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.seatmap-office-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.seatmap-office-tab {
|
||||
min-height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seatmap-office-tab.active {
|
||||
border-color: rgba(15, 118, 110, 0.22);
|
||||
background: rgba(15, 118, 110, 0.1);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.seatmap-topbar h3,
|
||||
.seatmap-panel-head h4 {
|
||||
margin: 0;
|
||||
@@ -432,6 +578,60 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.seatmap-actions[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.seatmap-actions .ghost-button {
|
||||
min-height: 40px;
|
||||
padding: 0 16px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 12px;
|
||||
letter-spacing: -0.01em;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
#seatmap-admin-save-btn {
|
||||
border-color: var(--color-brand-soft);
|
||||
background: var(--color-brand-soft);
|
||||
color: #fffaf3;
|
||||
}
|
||||
|
||||
#seatmap-admin-save-btn:hover:not(:disabled) {
|
||||
background: var(--color-brand);
|
||||
border-color: var(--color-brand);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-float);
|
||||
}
|
||||
|
||||
#seatmap-admin-save-btn:disabled {
|
||||
opacity: 1;
|
||||
cursor: not-allowed;
|
||||
border-color: rgba(26, 86, 69, 0.24);
|
||||
background: rgba(26, 86, 69, 0.18);
|
||||
color: rgba(16, 37, 29, 0.72);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#seatmap-admin-exit-btn,
|
||||
#seatmap-readonly-exit-btn {
|
||||
border-color: rgba(214, 138, 58, 0.48);
|
||||
background: rgba(242, 196, 132, 0.22);
|
||||
color: var(--color-brand-deep);
|
||||
}
|
||||
|
||||
#seatmap-admin-exit-btn:hover,
|
||||
#seatmap-readonly-exit-btn:hover {
|
||||
background: rgba(242, 196, 132, 0.34);
|
||||
border-color: rgba(182, 110, 34, 0.56);
|
||||
color: var(--color-brand);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.seatmap-status {
|
||||
@@ -456,6 +656,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.seatmap-board-panel,
|
||||
@@ -468,6 +669,7 @@
|
||||
|
||||
.seatmap-board-panel {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -475,7 +677,7 @@
|
||||
.seatmap-board-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
border-radius: 24px;
|
||||
background: #ffffff;
|
||||
padding: 0;
|
||||
@@ -490,6 +692,7 @@
|
||||
.seatmap-board {
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.seatmap-dxf-canvas {
|
||||
@@ -504,17 +707,31 @@
|
||||
}
|
||||
|
||||
.seatmap-dxf-frame-shell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 720px;
|
||||
min-height: 0;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.seatmap-dxf-drop-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.seatmap-dxf-drop-overlay.is-active {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.seatmap-dxf-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 720px;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
@@ -924,11 +1141,78 @@
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.seatmap-member-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.seatmap-member-section + .seatmap-member-section {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.seatmap-member-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.seatmap-member-section-head strong {
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.seatmap-member-section-head span {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.seatmap-member-section-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.seatmap-member-list .seatmap-member-card {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
}
|
||||
|
||||
.seatmap-member-search-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.seatmap-member-badge {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.seatmap-member-badge.occupied {
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.seatmap-member-card-compact {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
@@ -1005,24 +1289,48 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.seatmap-list-empty-inline {
|
||||
min-height: 64px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.dashboard-header {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
position: static;
|
||||
transform: none;
|
||||
order: 3;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
order: 2;
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-wrap: wrap;
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.header-date-controls {
|
||||
order: 2;
|
||||
width: auto;
|
||||
justify-content: flex-start;
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.seatmap-content {
|
||||
@@ -1062,8 +1370,13 @@
|
||||
}
|
||||
|
||||
.seatmap-topbar {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.seatmap-office-tabs {
|
||||
grid-column: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.seatmap-dxf-canvas {
|
||||
@@ -1076,7 +1389,7 @@
|
||||
|
||||
.seatmap-dxf-frame-shell,
|
||||
.seatmap-dxf-frame {
|
||||
min-height: 620px;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
incoming-files/1.png
Normal file
BIN
incoming-files/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 748 KiB |
2598
incoming-files/260320.html
Normal file
2598
incoming-files/260320.html
Normal file
File diff suppressed because one or more lines are too long
BIN
incoming-files/MH.xlsx
Normal file
BIN
incoming-files/MH.xlsx
Normal file
Binary file not shown.
34
incoming-files/README.md
Normal file
34
incoming-files/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# incoming-files Layout
|
||||
|
||||
`8081` 1차 구조 정리 기준으로 `incoming-files`는 아래처럼 해석한다.
|
||||
|
||||
## Served
|
||||
|
||||
- 실제 URL에서 직접 서빙되는 HTML
|
||||
- 현재 사용 파일:
|
||||
- `served/payment.html`
|
||||
- `served/mh.html`
|
||||
|
||||
주의:
|
||||
|
||||
- backend `/integrations/payment`, `/integrations/mh`는 위 `served/*`만 읽는다.
|
||||
- 새 기능을 붙일 때도 실제 서비스 파일은 `served/` 기준으로 수정한다.
|
||||
|
||||
## Reference
|
||||
|
||||
- 원본 참고 자산
|
||||
- 복구 비교용 자산
|
||||
- 직접 서빙하지 않는 파일
|
||||
|
||||
예:
|
||||
|
||||
- 원본 `xlsx`, `csv`
|
||||
- 샘플 스타일 파일
|
||||
- 원본/백업 HTML
|
||||
- 디자인 비교용 파일
|
||||
|
||||
## Temporary Comparison Copies
|
||||
|
||||
- 현재 루트의 `payment.html`, `mh.html`은 당장 삭제하지 않는다.
|
||||
- 이 두 파일은 기존 recovery 작업본과 현재 `served/*`를 비교하거나 되돌릴 때만 본다.
|
||||
- 다음 차수에서 안전성이 확보되면 `reference/` 하위로 재배치 여부를 검토한다.
|
||||
3472
incoming-files/mh.html
Normal file
3472
incoming-files/mh.html
Normal file
File diff suppressed because it is too large
Load Diff
3308
incoming-files/omh.html
Normal file
3308
incoming-files/omh.html
Normal file
File diff suppressed because it is too large
Load Diff
1571
incoming-files/opayment.html
Normal file
1571
incoming-files/opayment.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
incoming-files/organization.xlsx
Normal file
BIN
incoming-files/organization.xlsx
Normal file
Binary file not shown.
865
incoming-files/payment.csv
Normal file
865
incoming-files/payment.csv
Normal file
@@ -0,0 +1,865 @@
|
||||
상신회사,청구일,발행일,발행월,계정코드,관리계정코드,각사 계정명,프로젝트코드,사업명,사업명(표출PJT),사업명(인트라넷기준),사업분야,세부분야,기획/개발/영업,대분류,중분류,소분류,부서명,팀명,거래처,적요,차변공급가,대변공급가,지출,수입,특이사항,구분,프로젝트성격,,,,,,,,,
|
||||
바론,2025-12-08,2026-01-02,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"31,590",0,"31,590",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-08,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"31,590",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-05,2026-01-02,1월,10111101,REV-101,용역미수금(계산서발행),Y25040,보은국토 도로유지보수분야 통합건설사업관리용역(한맥/동성/동명)-인쇄편집,보은국토도로 사업관리,보은국토도로 사업관리,직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,(주)동성엔지니어링,보은국토도로유지보수분야통합건설사업관리용역(한맥/동성/동명)-인쇄편집,"1,815,000",0,0,"1,815,000",,수입,일반,,,,,,,,,
|
||||
바론,2025-12-05,2026-01-02,1월,40110501,REV-101,개발수입,Y25040,보은국토 도로유지보수분야 통합건설사업관리용역(한맥/동성/동명)-인쇄편집,보은국토도로 사업관리,보은국토도로 사업관리,직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,(주)동성엔지니어링,보은국토도로유지보수분야통합건설사업관리용역(한맥/동성/동명)-인쇄편집,0,"1,650,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2025-12-05,2026-01-02,1월,20112901,LIA-101,매출세액,Y25040,보은국토 도로유지보수분야 통합건설사업관리용역(한맥/동성/동명)-인쇄편집,보은국토도로 사업관리,보은국토도로 사업관리,직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,(주)동성엔지니어링,"1,650,000*10%",0,"165,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-02,1월,10111101,REV-101,용역미수금(계산서발행),Y25008,계양-강화간 고속도로 건설공사 기본설계단계 교량 BIM 설계(제5공구),계양~강화 고속도로(5공구),계양-강화 고속도로 건설 기본 및 실시(5공구),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)동해종합기술공사,계양-강화간고속도로건설공사기본설계단계교량BIM설계(제5공구),"28,500,000",0,0,"28,500,000",,수입,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-02,1월,40110501,REV-101,개발수입,Y25008,계양-강화간 고속도로 건설공사 기본설계단계 교량 BIM 설계(제5공구),계양~강화 고속도로(5공구),계양-강화 고속도로 건설 기본 및 실시(5공구),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)동해종합기술공사,계양-강화간고속도로건설공사기본설계단계교량BIM설계(제5공구),0,"25,909,091",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-02,1월,20112901,LIA-101,매출세액,Y25008,계양-강화간 고속도로 건설공사 기본설계단계 교량 BIM 설계(제5공구),계양~강화 고속도로(5공구),계양-강화 고속도로 건설 기본 및 실시(5공구),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)동해종합기술공사,"25,909,091*10%",0,"2,590,909",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-15,2026-01-02,1월,10111101,REV-101,용역미수금(계산서발행),Y25007,ERP시스템 구축,ERP시스템구축(한종),ERP(한국종합엔지니어링),직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,(주)한국종합엔지니어링,ERP시스템구축,"55,000,000",0,0,"55,000,000",,수입,일반,,,,,,,,,
|
||||
바론,2025-12-15,2026-01-02,1월,40110501,REV-101,개발수입,Y25007,ERP시스템 구축,ERP시스템구축(한종),ERP(한국종합엔지니어링),직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,(주)한국종합엔지니어링,ERP시스템구축,0,"50,000,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2025-12-15,2026-01-02,1월,20112901,LIA-101,매출세액,Y25007,ERP시스템 구축,ERP시스템구축(한종),ERP(한국종합엔지니어링),직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,(주)한국종합엔지니어링,"50,000,000*10%",0,"5,000,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-29,2026-01-02,1월,10111101,REV-101,용역미수금(계산서발행),Y25067,EG-BIM 사용 계약,EG-BIM (파이프텍코리아),EG-BIM (파이프텍코리아),직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,파이프텍코리아,EG-BIM사용계약,"1,100,000",0,0,"1,100,000",,수입,일반,,,,,,,,,
|
||||
바론,2025-12-29,2026-01-02,1월,40110601,REV-101,유지관리수입,Y25067,EG-BIM 사용 계약,EG-BIM (파이프텍코리아),EG-BIM (파이프텍코리아),직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,파이프텍코리아,EG-BIM사용계약,0,"1,000,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2025-12-29,2026-01-02,1월,20112901,LIA-101,매출세액,Y25067,EG-BIM 사용 계약,EG-BIM (파이프텍코리아),EG-BIM (파이프텍코리아),직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,파이프텍코리아,"1,000,000*10%",0,"100,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,총괄기획실 세미나 후 회식,0,"570,500",0,0,공통 → 인사교육,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,60115705,WF-101,복리후생비(회식대),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,총괄기획실 세미나 후 회식,"570,500",0,"570,500",0,공통 → 인사교육,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-22,2026-01-02,1월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비,"30,400",0,"30,400",0,,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-22,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비 - 김원식,0,"30,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-02,1월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비,"106,200",0,"106,200",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비 - 김원식,0,"106,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,총괄기획실,김원식,시내교통비,0,"44,900",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,50152705,WF-201,원가)여비교통비(시내교통비),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,총괄기획실,김원식,시내교통비,"44,900",0,"44,900",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,사장단회의 다과(1월 2일),0,"181,840",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-07,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,"바론 인감증명서, 등기부등본 발급수수료","6,000",0,"6,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,우편요금 반송 수수료,0,"2,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-05,2026-01-15,1월,60114398,OP-203,소모품비(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,바론 입찰용 사용인감 제작,"78,800",0,"78,800",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,(주)교보문고,기술개발센터도서구매(12월),0,"253,260",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-05,2026-01-15,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,"바론 입찰용 등기부등본, 인감증명서 발급","25,000",0,"25,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,외부인사 초청 세미나(유니스트 교수) 다과,0,"26,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,송파구청,바론 2026 등록면허세 납부(측량업),"27,000",0,"27,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,"노션 구독료(25.12~25.12, 사내가이드)",0,"187,420",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,송파구청,바론 2026 등록면허세 납부(정보통신공사업),"54,000",0,"54,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,마천사무실 주차 할인권 구매,0,"60,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,송파구청,바론 2026 등록면허세 납부(초경량비행장치업),"40,500",0,"40,500",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 설치용 USB 오피스 파우치 구매(2개),0,"31,700",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-29,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,법인 서류 등기,360,0,360,0,,구매,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,20111105,LIA-101,미지급금(KB국민카드),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,,국민카드(3866),스타벅스 기프트카드 구매,0,"1,000,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-29,1월,60114301,OP-203,소모품비(사무용품비),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,날인용 도장(7개),"20,900",0,"20,900",0,,구매,일반,,,,,,,,,
|
||||
바론,2025-12-16,2026-01-02,1월,10111101,REV-101,용역미수금(계산서발행),Y25042,예산국토 제2권역 도로유지보수사업 통합건설사업관리용역(한맥/동성/장맥)-인쇄/편집,예산국토 제2권역,예산국토 제2권역,직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,장맥ENG,예산국토제2권역도로유지보수사업통합건설사업관리용역(한맥/동성/장맥)-인쇄/편집,"1,512,500",0,0,"1,512,500",,수입,일반,,,,,,,,,
|
||||
바론,2025-12-16,2026-01-02,1월,40110501,REV-101,개발수입,Y25042,예산국토 제2권역 도로유지보수사업 통합건설사업관리용역(한맥/동성/장맥)-인쇄/편집,예산국토 제2권역,예산국토 제2권역,직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,장맥ENG,예산국토제2권역도로유지보수사업통합건설사업관리용역(한맥/동성/장맥)-인쇄/편집,0,"1,375,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2025-12-16,2026-01-02,1월,20112901,LIA-101,매출세액,Y25042,예산국토 제2권역 도로유지보수사업 통합건설사업관리용역(한맥/동성/장맥)-인쇄/편집,예산국토 제2권역,예산국토 제2권역,직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,장맥ENG,"1,375,000*10%",0,"137,500",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,정성호,출장비,"52,600",0,"52,600",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,정성호,출장비 - 정성호,0,"52,600",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"23,630",0,"23,630",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"23,630",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-05,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,(주)미래엔서해에너지,당진 숙소 임대 종료에 따른 도시가스 정산,0,"41,870",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,장헌산업 등기관련 인감증명서 수수료,"5,400",0,"5,400",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,장헌 사내이사 서류 발급 수수료,"2,200",0,"2,200",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,"바론 인감증명서, 등기부등본 발급수수료",0,"6,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,"법인명변경/법인분할 등기 관련 서류 발급 수수료(인감증명서 4, 등본 2, 초본 2)","4,000",0,"4,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2025-12-19,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),X21012,XR기반 건설 설계 혁신 시스템,XR기반 건설설계 혁신시스템,XR기반 건설설계 혁신시스템,직접매출,R&D,,매출,가족사 프로젝트,R&D,총괄기획실,경영기획팀,임민경,출장비,"24,000",0,"24,000",0,,출장비,R&D,,,,,,,,,
|
||||
바론,2025-12-19,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X21012,XR기반 건설 설계 혁신 시스템,XR기반 건설설계 혁신시스템,XR기반 건설설계 혁신시스템,직접매출,R&D,,매출,가족사 프로젝트,R&D,총괄기획실,경영기획팀,임민경,출장비 - 임민경,0,"24,000",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,총괄기획실 외빈 선물용 와인 구매(5EA),0,"230,000",0,0,,제외,일반,,,,,,,,,
|
||||
장헌산업,2026-01-22,2026-01-22,1월,831,OUT-201,지급수수료,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사 조수호,주식회사 장헌산업 상호변경 공과금납부,"465,000",,"465,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),X25020,TOVA,TOVA,TOVA,S/W개발,교통,기획,비매출,S/W 개발,교통,총괄기획실,기술기획팀,황동환,출장비,"47,300",0,"47,300",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25020,TOVA,TOVA,TOVA,S/W개발,교통,,비매출,S/W 개발,교통,총괄기획실,기술기획팀,김원기,출장비 - 김원기,0,"39,300",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25020,TOVA,TOVA,TOVA,S/W개발,교통,,비매출,S/W 개발,교통,총괄기획실,기술기획팀,황동환,출장비 - 황동환,0,"8,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),X25037,ERP: 장헌산업,ERP:장헌산업,ERP: 장헌산업,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,총괄기획실,ERP기획팀,권오재,출장비,"190,040",0,"190,040",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25037,ERP: 장헌산업,ERP:장헌산업,ERP: 장헌산업,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,총괄기획실,ERP기획팀,류호성,출장비 - 류호성,0,"89,310",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25037,ERP: 장헌산업,ERP:장헌산업,ERP: 장헌산업,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,총괄기획실,ERP기획팀,권오재,출장비 - 권오재,0,"100,730",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,25년 12월 4대보험료 (전체),0,"58,112,940",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,50152527,HR-204,원가)복리후생비(산재보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,25년 12월 4대보험료(산재보험/회사부담),"2,725,290",0,"2,725,290",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,50152521,HR-202,원가)복리후생비(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,25년 12월 4대보험료(건강보험/회사부담),"12,218,640",0,"12,218,640",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,20111505,HR-202,예수금(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,25년 12월 4대보험료(건강보험/직원부담),"12,218,640",0,"12,218,640",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,50152521,HR-202,원가)복리후생비(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,25년 12월 4대보험료(요양보험/회사부담),"1,581,980",0,"1,581,980",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,20111505,HR-202,예수금(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,25년 12월 4대보험료(요양보험),"1,581,980",0,"1,581,980",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,50151719,HR-201,원가)세금과공과(국민연금),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민연금관리공단,25년 12월 4대보험료(국민연금/회사부담),"10,816,770",0,"10,816,770",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,20111507,HR-201,예수금(국민연금),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민연금관리공단,25년 12월 4대보험료(국민연금/직원부담),"10,816,770",0,"10,816,770",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,50152529,HR-203,원가)복리후생비(고용보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,25년 12월 4대보험료(고용보험/회사부담),"3,467,490",0,"3,467,490",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-07,1월,20111509,HR-203,예수금(고용보험료(직원)),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,25년 12월 4대보험료(고용보험/직원부담),"2,685,380",0,"2,685,380",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-09,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,인프라 BIM1팀,안효원,삼안 소속 경조금 선지급 (안효원 선임연구원 본인 결혼),0,"500,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
장헌산업,2026-01-27,2026-01-27,1월,831,OUT-201,지급수수료,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사 조수호,주식회사 장헌파트너스 분할 관련 공과금 납부,"6,680,000",,"6,680,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-12,1월,10111101,REV-101,용역미수금(계산서발행),Y25041,GAIA 기능 개선 계약,GAIA 기능 개선 계약,GAIA 기능 개선 계약,직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,(주)지오메카이엔지,GAIA앱 기능추가 개발,"3,850,000",0,0,"3,850,000",,수입,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-12,1월,40110501,REV-101,개발수입,Y25041,GAIA 기능 개선 계약,GAIA 기능 개선 계약,GAIA 기능 개선 계약,직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,(주)지오메카이엔지,GAIA앱 기능추가 개발,0,"3,500,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-12,1월,20112901,LIA-101,매출세액,Y25041,GAIA 기능 개선 계약,GAIA 기능 개선 계약,GAIA 기능 개선 계약,직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,(주)지오메카이엔지,"3,500,000*10%",0,"350,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-12,1월,10111101,REV-101,용역미수금(계산서발행),Y25041,GAIA 기능 개선 계약,GAIA 기능 개선 계약,GAIA 기능 개선 계약,직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,(주)지오메카이엔지,GAIA앱 기능추가 개발,0,"3,850,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-12,1월,10110501,AST-101,보통예금,Y25041,GAIA 기능 개선 계약,GAIA 기능 개선 계약,GAIA 기능 개선 계약,직접매출,S/W판매,,매출,바론계약,판매,기술개발센터,,국민(주거래),GAIA앱 기능추가 개발,"3,850,000",0,0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-14,1월,20110101,LIA-101,외상매입금,Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,주식회사 비젼테크아이엔씨,서산-아산 건설사업단 대회의실 긴급 수리 비용,0,"253,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-14,1월,50151399,OP-203,원가)소모품비(기타),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,,대덕테크놀로지 주식회사,드론 사진측량 실무 프로젝트 수행을 위한 공구(드릴) 구매의 건,"754,909",0,"754,909",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-14,1월,10115301,AST-106,매입세액,Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,,대덕테크놀로지 주식회사,드론 사진측량 실무 프로젝트 수행을 위한 공구(드릴) 구매의 건,"75,491",0,"75,491",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-11,2026-01-14,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,소모품 (창고 물품보관용가방) 구입,0,"13,970",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-21,1월,50151399,OP-203,원가)소모품비(기타),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,,(주)코세코,드론 사진측량 실무 프로젝트 수행을 위한 장비 구매의 건,"349,910",0,"349,910",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-14,1월,20110101,LIA-101,외상매입금,Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,,대덕테크놀로지 주식회사,드론 사진측량 실무 프로젝트 수행을 위한 공구(드릴) 구매의 건,0,"830,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-21,1월,10115301,AST-106,매입세액,Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,,(주)코세코,드론 사진측량 실무 프로젝트 수행을 위한 장비 구매의 건,"34,990",0,"34,990",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-20,1월,50153199,IT-301,원가)지급수수료(기타),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,Infra Solution 개발팀,최정우,파견인력 변경 관련 서류 발급수수료,"47,400",0,"47,400",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-15,1월,20110101,LIA-101,외상매입금,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,몬스타 주식회사,인프라 BIM 1팀 측량 결과물 3D 모델링을 위한 워크스테이션 긴급 수리,0,"600,000",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2026-01-12,2026-01-14,1월,60114599,WF-105,도서인쇄비(기타),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,,,대산당진 시공BIM 수행/도서인쇄비,"306,240",,"306,240",0,,구매,일반,,,,,,,,,
|
||||
한맥,2026-01-12,2026-01-14,1월,50151309,OP-203,원가)소모품비(안전관리장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,,,대산당진 시공BIM 안전장비 구매,"390,940",,"390,940",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-11,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,관리실 방문 음료,0,"47,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-14,1월,50153199,IT-301,원가)지급수수료(기타),Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,주식회사 비젼테크아이엔씨,서산-아산 건설사업단 대회의실 긴급 수리 비용,"230,000",0,"230,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20110101,LIA-101,외상매입금,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 대표전화 통신비 26년 01월,0,"23,780",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-14,1월,10115301,AST-106,매입세액,Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,주식회사 비젼테크아이엔씨,서산-아산 건설사업단 대회의실 긴급 수리 비용,"23,000",0,"23,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-21,1월,60116199,OP-204,통신비(기타),Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,SK브로드밴드(주),서산-아산 건설사업단 BIG ROOM VPN 26년 01월 결제의 건,"86,000",0,"86,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-21,1월,10115301,AST-106,매입세액,Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,SK브로드밴드(주),서산-아산 건설사업단 BIG ROOM VPN 26년 01월 결제의 건,"8,600",0,"8,600",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,70111501,REV-501,잡이익,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 대표전화 통신비 26년 01월,0,8,0,0,,수입,일반,,,,,,,,,
|
||||
바론,2026-01-05,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,"바론 입찰용 사용인감 제작, 등기부등본 발급수수료",0,"103,800",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-09,1월,50152511,WF-106,원가)복리후생비(경조금),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,인프라 BIM1팀,안효원,삼안 소속 경조금 선지급 (안효원 선임연구원 본인 결혼),"500,000",0,"500,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-19,1월,60116399,IT-301,지급수수료(기타),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 CHAT GPT 구독료(1월),"29,000",0,"29,000",0,공통 → 경영진,구매,기타,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-19,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 CHAT GPT 구독료(1월),0,"29,000",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-27,1월,60115798,WF-106,복리후생비(공통),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,한상연,건강검진 비용 청구(한상연 부사장),"200,000",0,"200,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-19,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,0,"55,000",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-19,1월,60115705,WF-101,복리후생비(회식대),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,"55,000",0,"55,000",0,공통 → 경영진,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,주식회사 헬셀,"KB 드론책임배상보험 신규 가입의 건(총 2건 / Matrice 300 RTK, Air2s)",0,"844,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-28,1월,60115798,WF-106,복리후생비(공통),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,서울용한치과의원,임직원 건강검진(치아) 비용 정산의 건(센터 및 총괄 39명),"195,000",0,"195,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"32,240",0,"32,240",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"32,240",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/07),0,"59,100",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-01-29,1월,60116399,IT-301,지급수수료(기타),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 AI 구독료(1월),"29,000",0,"29,000",0,공통 → 경영진,구매,기타,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,20111105,LIA-101,미지급금(KB국민카드),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,,국민카드(3866),스타벅스 기프트카드 구매,0,"700,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,60115798,WF-106,복리후생비(공통),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,서울용한치과의원,임직원 건강검진(치아) 비용 정산의 건(바론소속 3명),"15,000",0,"15,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25059,ERP: 장헌,ERP:장헌,ERP: (주)장헌,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,총괄기획실,기술기획팀,김원기,출장비,"186,300",0,"186,300",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25059,ERP: 장헌,ERP:장헌,ERP: (주)장헌,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,총괄기획실,기술기획팀,김원기,출장비 - 김원기,0,"126,300",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25059,ERP: 장헌,ERP:장헌,ERP: (주)장헌,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,총괄기획실,기술기획팀,홍아름,출장비 - 홍아름,0,"30,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25059,ERP: 장헌,ERP:장헌,ERP: (주)장헌,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,총괄기획실,기술기획팀,김혜인,출장비 - 김혜인,0,"30,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"46,400",0,"46,400",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"46,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,출장비,"90,830",0,"90,830",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,출장비 - 류원준,0,"90,830",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"36,470",0,"36,470",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"36,470",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,EG-BIM 각부서 실사용자와 개발자 미팅(삼안),0,"39,820",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,EG-BIM 각부서 실사용자와 개발자 미팅(삼안),"39,820",0,"39,820",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,송파구청,바론 2026 등록면허세 납부(측량업),0,"27,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,60116399,IT-301,지급수수료(기타),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,경영기획팀,임민경,건설기술인 연회비(조용민) 경력증명서 2부(정희욱) 수수료,"23,000",0,"23,000",0,,구매,공통,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,송파구청,바론 2026 등록면허세 납부(정보통신공사업),0,"54,000",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2025-12-15,2026-01-08,1월,50152599,WF-106,원가)복리후생비(기타),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,,2025년 건강검진비,"200,000",,"200,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,송파구청,바론 2026 등록면허세 납부(초경량비행장치업),0,"40,500",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2025-12-31,2026-01-05,1월,50152599,WF-106,원가)복리후생비(기타),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/커피원두 구매_25년 12월,"660,000",,"660,000",0,X,구매,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,개발소프트웨어 영업(01/9),0,"45,400",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2025-12-31,2026-01-05,1월,50151309,OP-203,원가)소모품비(안전관리장비),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/페이퍼타올 구매(25년 12월),"140,800",,"140,800",0,X,구매,공통,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,시연행사 다과 구입,0,"50,000",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2025-12-31,2026-01-05,1월,50151101,OP-101,원가)지급임차료(월세),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,,,월세 : 대산-당진(제2공구) 시공BIM 용역 현장 직원 숙소 계약,"3,479,500",,"3,479,500",0,공통 → 대산당진BIM,구매,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"39,290",0,"39,290",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"39,290",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/09),0,"52,300",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2026-01-07,2026-01-08,1월,50153103,OUT-102,원가)지급수수료(용역수수료),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,기술개발센터 ADT캡스 월정료_26년 1월,"99,000",,"99,000",0,X,구매,공통,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,"과천(삼안), 홍천(내경), 일산(서영)",0,"129,158",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,"과천(삼안), 홍천(내경), 일산(서영)","129,158",0,"129,158",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/15),0,"22,200",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2026-01-08,2026-01-08,1월,60115707,WF-106,복리후생비(경조금),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,,기술개발센터 안효원 선임연구원 결혼 축의금,"300,000",,"300,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,ERP기획팀,송대일,이지빔 도메인 연장 2건 (3년),0,"141,020",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-14,1월,60116101,IT-401,"통신비(전화,TV,인터넷)",ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/통신요금(26년 01월),"615,055",,"615,055",0,X,구매,공통,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-20,1월,20111109,LIA-101,미지급금(하나카드),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,매일경제신문사(매경이코노미),매일경제 구독료(12월),0,"25,000",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2025-12-31,2026-01-06,1월,60115707,WF-106,복리후생비(경조금),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,,정활상 부친상/중앙대장례식장1호실,"300,000",,"300,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"21,220",0,"21,220",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"21,220",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"29,960",0,"29,960",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"29,960",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25043,CivilEngineeringLab,CivilEngineeringLab,CivilEngineeringLab,기획/관리,기획&관리,,비매출,S/W 개발,운영S/W,총괄기획실,ERP기획팀,송대일,rightcivilengineering 도메인 구입(3년),0,"68,970",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115798,WF-106,복리후생비(공통),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 임직원 체력단련비(김흥제),"601,400",0,"601,400",0,,구매,공통,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,"문서자동화 기획 AI구입(chatGPT, NotebookLM)",0,"61,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,60116199,OP-204,통신비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),12월 휴대폰 사용요금(최대선),"37,633",0,"37,633",0,,구매,공통,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-20,1월,20110101,LIA-101,외상매입금,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,씨앤피,11월 바론 명함 대금,0,"99,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,10115301,AST-106,매입세액,OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),12월 휴대폰 사용요금(최대선),"3,762",0,"3,762",0,,구매,공통,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,60116199,OP-204,통신비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),12월 휴대폰 사용요금(최대선),"102,885",0,"102,885",0,,구매,공통,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-20,1월,20110101,LIA-101,외상매입금,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 ERP서버 인터넷 회선 통신비 26년 01월 결제의 건,0,"561,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,50151399,OP-203,원가)소모품비(기타),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 스토리지 12월 금액,"19,474",0,"19,474",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,50152999,OP-204,원가)통신비(기타),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,LTE 태블릿 통신비 청구(11월사용),"35,200",0,"35,200",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-20,1월,20110101,LIA-101,외상매입금,X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,기술개발센터,,The Intellicad Technology Consortium,EG-BIM 개발 및 사용을 위한 ITC (IntelliCAD) 로열티 지급의 건(외화),0,"51,569,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-09,1월,50151399,OP-203,원가)소모품비(기타),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 서버 12월 사용 금액,"290,556",0,"290,556",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-21,1월,20110101,LIA-101,외상매입금,Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,,(주)코세코,드론 사진측량 실무 프로젝트 수행을 위한 장비 구매의 건,0,"384,900",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,50151399,OP-203,원가)소모품비(기타),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 스토리지 01월 금액,"23,705",0,"23,705",0,"원가)소모품비(기타) 코드가 두개임 50151309, 50151399 (하나로 통일 필요+계산을 위해 임의로 50151399로 변경)",구매,일반,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,50151399,OP-203,원가)소모품비(기타),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,"문서자동화 기획 AI구입(chatGPT, NotebookLM)","61,200",0,"61,200",0,원가)소모품비(기타) 코드가 두개임,구매,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-21,1월,20110101,LIA-101,외상매입금,Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,SK브로드밴드(주),서산-아산 건설사업단 BIG ROOM VPN 26년 01월 결제의 건,0,"94,600",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2026-01-02,2026-01-08,1월,60114733,IT-301,경상시험연구비(지급수수료),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,총괄기획실,,,스마트건설10/특허등록,"2,057,000",,"2,057,000",0,,구매,R&D,,,,,,,,,
|
||||
한맥,2026-01-15,2026-01-15,1월,60114733,IT-301,경상시험연구비(지급수수료),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,총괄기획실,,,스마트건설10/사무용품 구매,"1,469,050",,"1,469,050",0,,구매,R&D,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-21,1월,20110101,LIA-101,외상매입금,X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,구글클라우드 Ai 사용료(12월),0,"115,624",0,0,,제외,일반,,,,,,,,,
|
||||
장헌산업,2026-01-15,2026-01-20,1월,823,OUT-101,연구개발비,X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,드림디포,스마트건설/사무용품 구매(일부법인지급),"937,530",,"937,530",0,관리계정=연구개발/ 실질은 비품,구매,R&D,,,,,,,,,
|
||||
장헌산업,2026-01-20,2026-01-20,1월,823,OUT-101,연구개발비,X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,(사)국토교통과학기술진흥원,스마트건설/4분기(10월~12월) 부가세,"264,000",,"264,000",0,관리계정=연구개발/ 실질은 부가세,구매,R&D,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20110101,LIA-101,외상매입금,X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,구글클라우드 Ai 사용료(11월),0,589,0,0,,제외,일반,,,,,,,,,
|
||||
피티씨,2026-01-05,2026-01-15,1월,823,OUT-101,연구개발비,X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,드림디포거여점,[스마트건설] 12월 사무용품비(일부법인지급),"1,908,170",,"1,908,170",0,관리계정=연구개발/ 실질은 비품,구매,R&D,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,60115798,WF-106,복리후생비(공통),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,사장단회의 다과(1월 2일),"181,840",0,"181,840",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111109,LIA-101,미지급금(하나카드),X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,하나카드(6669),Ai셀 Cloudflare 사용료(12월),0,"8,375",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,60116399,IT-301,지급수수료(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,우편요금 반송 수수료,"2,400",0,"2,400",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,60110101,HR-101,급여(임.직원),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,총괄/장종찬,26년 1월 급여(바론),"388,889,600",0,"388,889,600",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111501,LIA-101,예수금(근로소득세),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,26년1월 급여 근로소득세(바론),0,"29,627,210",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111503,LIA-101,예수금(근로소득주민세),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,26년1월 급여 근로소득주민세(바론),0,"2,962,370",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111509,HR-203,예수금(고용보험료(직원)),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,26년1월 급여 고용보험(바론),0,"3,260,540",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111505,HR-202,예수금(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,"26년1월 급여 의료보험(건강+요양, 바론)",0,"14,146,850",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111507,HR-201,예수금(국민연금),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,26년1월 급여 국민연금(바론),0,"11,756,650",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111599,LIA-101,예수금(기타),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,26년1월 식대 및 기타 공제(바론),0,"1,988,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,총괄/장종찬,26년1월 급여(바론),0,"325,147,580",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-22,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,1월 업무관련 유류대,0,"311,750",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-22,1월,60115999,WF-201,여비교통비(기타),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,1월 업무관련 유류대,"311,750",0,"311,750",0,공통 → 경영진,출장비,기타,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-22,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,0,"27,000",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-22,1월,60115705,WF-101,복리후생비(회식대),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,"27,000",0,"27,000",0,공통 → 경영진,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-22,1월,20110101,LIA-101,외상매입금,X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,주식회사 케이지에듀원,바론컨설턴트 2025년 법정필수 및 산업안전보건교육,0,"1,332,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,60114503,WF-105,도서인쇄비(도서구입비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,(주)교보문고,기술개발센터도서구매(12월),"253,260",0,"253,260",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,2026경영및기술지원서비스(한맥기술),"122,916,200",0,0,"122,916,200",,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,40110401,REV-101,관리용역수입,Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,2026경영및기술지원서비스(한맥기술),0,"111,742,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,20112901,LIA-101,매출세액,Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,"111,742,000*10%",0,"11,174,200",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2025-12-10,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,경영및기술지원서비스(장헌산업),"28,270,000",0,0,"28,270,000",,수입,기타,,,,,,,,,
|
||||
바론,2025-12-10,2026-01-23,1월,40110401,REV-101,관리용역수입,Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,경영및기술지원서비스(장헌산업),0,"25,700,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2025-12-10,2026-01-23,1월,20112901,LIA-101,매출세액,Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,"25,700,000*10%",0,"2,570,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,2026경영및기술지원서비스(삼안),"294,607,500",0,0,"294,607,500",,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,40110401,REV-101,관리용역수입,Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,2026경영및기술지원서비스(삼안),0,"267,825,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,20112901,LIA-101,매출세액,Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,"267,825,000*10%",0,"26,782,500",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,2026경영및기술지원서비스(삼안),0,"294,607,500",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10110501,AST-101,보통예금,Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(삼안),"294,607,500",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,경영및기술지원서비스(장헌산업),0,"28,270,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10110501,AST-101,보통예금,Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,국민(주거래),경영및기술지원서비스(장헌산업),"28,270,000",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,2026경영및기술지원서비스(한맥기술),0,"122,916,200",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10110501,AST-101,보통예금,Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(한맥기술),"122,916,200",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,2026경영및기술지원서비스(한라산업개발),0,"10,285,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10110501,AST-101,보통예금,Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(한라산업개발),"10,285,000",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,2026경영및기술지원서비스(피티씨)1월,0,"23,760,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10110501,AST-101,보통예금,Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(피티씨)1월,"23,760,000",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26013,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(도화30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,도화ENG,한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(도화30%)-인쇄/편집,0,"1,815,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-23,1월,10110501,AST-101,보통예금,Y26013,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(도화30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,국민(주거래),한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(도화30%)-인쇄/편집,"1,815,000",0,0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-23,1월,60115903,WF-201,여비교통비(국내출장비),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,출장비,"82,000",0,"82,000",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-23,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,출장비 - 최근혜,0,"82,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-23,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,노트북가방 구매(01/18),0,"100,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-02,1월,60115798,WF-106,복리후생비(공통),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,외부인사 초청 세미나(유니스트 교수) 다과,"26,200",0,"26,200",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26013,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(도화30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,도화ENG,한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(도화30%)-인쇄/편집,"1,815,000",0,0,"1,815,000",,수입,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-23,1월,40110501,REV-101,개발수입,Y26013,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(도화30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,도화ENG,한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(도화30%)-인쇄/편집,0,"1,650,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-23,1월,20112901,LIA-101,매출세액,Y26013,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(도화30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,도화ENG,"1,650,000*10%",0,"165,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,2026경영및기술지원서비스(피티씨)1월,"23,760,000",0,0,"23,760,000",,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,40110401,REV-101,관리용역수입,Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,2026경영및기술지원서비스(피티씨)1월,0,"21,600,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,20112901,LIA-101,매출세액,Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,"21,600,000*10%",0,"2,160,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,10111101,REV-101,용역미수금(계산서발행),Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,2026경영및기술지원서비스(한라산업개발),"10,285,000",0,0,"10,285,000",,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,40110401,REV-101,관리용역수입,Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,2026경영및기술지원서비스(한라산업개발),0,"9,350,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-23,1월,20112901,LIA-101,매출세액,Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,"9,350,000*10%",0,"935,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-27,1월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,통합로그인 회의 후 회식비,0,"154,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-27,1월,60115705,WF-101,복리후생비(회식대),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,통합로그인 회의 후 회식비,"154,400",0,"154,400",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-27,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민은행,퇴직연금(국민) 2026년 1월분 (43명),0,"18,331,070",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-27,1월,60111101,HR-103,퇴직금,X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민은행,퇴직연금(국민) 2026년 1월분 (43명),"18,331,070",0,"18,331,070",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-27,1월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,신한은행,퇴직연금(신한) 2026년 1월분 (6명),0,"2,182,210",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-27,1월,60111101,HR-103,퇴직금,X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,신한은행,퇴직연금(신한) 2026년 1월분 (6명),"2,182,210",0,"2,182,210",0,X,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-27,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,한상연,건강검진 비용 청구(한상연 부사장),0,"200,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-02,1월,60114301,OP-203,소모품비(사무용품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,마천사무실 주차 할인권 구매,"60,000",0,"60,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-28,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,서울용한치과의원,임직원 건강검진(치아) 비용 정산의 건(센터 및 총괄 39명),0,"195,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2026-01-05,2026-01-07,1월,60113105,OP-103,수도광열비(상하수도),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,(주)미래엔서해에너지,11월 당진 숙소 도시가스 요금,"40,770",0,"40,770",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/15),0,"9,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-05,2026-01-07,1월,60113105,OP-103,수도광열비(상하수도),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,(주)미래엔서해에너지,12월 당진 숙소 도시가스 요금,"1,100",0,"1,100",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,"법인 등기 관련 비용(도장, 등기)",0,"21,260",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-07,1월,60114398,OP-203,소모품비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,총괄기획실 외빈 선물용 와인 구매(5EA),"230,000",0,"230,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-11,2026-01-14,1월,60114301,OP-203,소모품비(사무용품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,소모품 (창고 물품보관용가방) 구입,"13,970",0,"13,970",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 홍보목적 브로슈어 클리어파일 구매(100매),0,"21,700",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-15,1월,60113707,IT-205,수선비(전산),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,몬스타 주식회사,인프라 BIM 1팀 측량 결과물 3D 모델링을 위한 워크스테이션 긴급 수리,"545,455",0,"545,455",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,출장(당진 공장 등록 관련),"30,000",0,"30,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,출장비 - 김윤재,0,"30,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,ERP기획팀,송대일,eg-bim 홈페이지 호스팅 업그레이드,0,"38,140",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-14,2026-01-15,1월,10115301,AST-106,매입세액,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,몬스타 주식회사,인프라 BIM 1팀 측량 결과물 3D 모델링을 위한 워크스테이션 긴급 수리,"54,545",0,"54,545",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,ERP기획팀,송대일,egbim 홈페이지 트래픽 초기화,0,"2,750",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-11,2026-01-15,1월,50152599,WF-106,원가)복리후생비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,관리실 방문 음료,"47,000",0,"47,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,한라대 ( 원주 ) : 강원 BIM 아카데미 강의지원,0,"168,330",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,한라대 ( 원주 ) : 강원 BIM 아카데미 강의지원,"168,330",0,"168,330",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 AI 구독료(1월),0,"29,000",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60116199,OP-204,통신비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 대표전화 통신비 26년 01월,"1,626",0,"1,626",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,김우진,회계법인 업무미팅 후 식사,0,"15,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김우진A,회계법인 업무미팅 후 식사,"15,000",0,"15,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"64,030",0,"64,030",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"64,030",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"25,120",0,"25,120",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"25,120",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"99,830",0,"99,830",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"99,830",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111105,LIA-101,미지급금(KB국민카드),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,국민카드(8824),12월 업무관련 교통비(김흥제),0,"568,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"108,000",0,"108,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"74,000",0,"74,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"10,000",0,"10,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"100,000",0,"100,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"5,000",0,"5,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"94,000",0,"94,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"48,000",0,"48,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"11,000",0,"11,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 교통비(김흥제),"118,000",0,"118,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/09),0,"6,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60116199,OP-204,통신비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 대표전화 단말기할부금(부가세대상),"22,000",0,"22,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,서울용한치과의원,임직원 건강검진(치아) 비용 정산의 건(바론소속 3명),0,"15,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,10115301,AST-106,매입세액,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),공급가액*10%,162,0,162,0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,20111109,LIA-101,미지급금(하나카드),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,매일경제신문사(매경이코노미),매일경제 구독료(26년 1월),0,"25,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-20,1월,50151999,OP-201,원가)보험료(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,주식회사 헬셀,"KB 드론책임배상보험 신규 가입의 건(총 2건 / Matrice 300 RTK, Air2s)","844,000",0,"844,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"21,440",0,"21,440",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"21,440",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,2026 강원 BIM 아카데미 강의 지원(원주),"87,920",0,"87,920",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,2026 강원 BIM 아카데미 강의 지원(원주),0,"87,920",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111105,LIA-101,미지급금(KB국민카드),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,국민카드(8824),12월 업무관련 식대(김흥제),0,"988,600",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"200,000",0,"200,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"61,000",0,"61,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"161,000",0,"161,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"35,500",0,"35,500",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"14,100",0,"14,100",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"67,000",0,"67,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"250,000",0,"250,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,12월 업무관련 식대(김흥제),"200,000",0,"200,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,경영기획팀,임민경,건설기술인 연회비(조용민) 경력증명서 2부(정희욱) 수수료,0,"23,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-20,1월,60114301,OP-203,소모품비(사무용품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,시연행사 다과 구입,"50,000",0,"50,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,1월 총괄기획실 물품 및 다과 구매,0,"162,220",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-20,1월,60114501,WF-301,도서인쇄비(신문및정기간행물),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,매일경제신문사(매경이코노미),매일경제 구독료(12월),"25,000",0,"25,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111105,LIA-101,미지급금(KB국민카드),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,국민카드(8824),12월 임직원 체력단련비(김흥제),0,"601,400",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-20,1월,50151507,MK-101,원가)도서인쇄비(인쇄비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,씨앤피,11월 바론 명함 대금,"90,000",0,"90,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111109,LIA-101,미지급금(하나카드),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,하나카드(7726),12월 업무관련 유류대(최대선),0,"493,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,12월 업무관련 유류대(최대선),"71,000",0,"71,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,12월 업무관련 유류대(최대선),"72,000",0,"72,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,12월 업무관련 유류대(최대선),"114,000",0,"114,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,12월 업무관련 유류대(최대선),"76,000",0,"76,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,12월 업무관련 유류대(최대선),"76,000",0,"76,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,12월 업무관련 유류대(최대선),"84,000",0,"84,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,장헌산업 등기관련 인감증명서 수수료,0,"5,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-20,1월,10115301,AST-106,매입세액,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,씨앤피,11월 바론 명함 대금,"9,000",0,"9,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111105,LIA-101,미지급금(KB국민카드),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,국민카드(8824),1월 업무관련 교통비(김흥제),0,"272,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"50,000",0,"50,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"5,000",0,"5,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"13,000",0,"13,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"7,000",0,"7,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"7,000",0,"7,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"50,000",0,"50,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"50,000",0,"50,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115999,WF-201,여비교통비(기타),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 교통비(김흥제),"90,000",0,"90,000",0,,출장비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,장헌 사내이사 서류 발급 수수료,0,"2,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-20,1월,60116199,OP-204,통신비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 ERP서버 인터넷 회선 통신비 26년 01월 결제의 건,"510,000",0,"510,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111105,LIA-101,미지급금(KB국민카드),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,국민카드(8824),1월 업무관련 식대(김흥제),0,"1,717,300",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"62,500",0,"62,500",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"28,000",0,"28,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"200,000",0,"200,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"221,000",0,"221,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"25,000",0,"25,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"250,000",0,"250,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"315,000",0,"315,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"242,000",0,"242,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"241,000",0,"241,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"100,000",0,"100,000",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,김흥제,1월 업무관련 식대(김흥제),"32,800",0,"32,800",0,,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-29,1월,10111101,REV-101,용역미수금(계산서발행),Y25017,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(광터교차로) BIM 설계,국도42 원주~흥업,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(삼안),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)진화기술공사,국도42호선원주흥업사제외1개소교차로개선공사실시설계용역(광터교차로)BIM설,"14,520,000",0,0,"14,520,000",,수입,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-29,1월,40110501,REV-101,개발수입,Y25017,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(광터교차로) BIM 설계,국도42 원주~흥업,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(삼안),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)진화기술공사,국도42호선원주흥업사제외1개소교차로개선공사실시설계용역(광터교차로)BIM설,0,"13,200,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-29,1월,20112901,LIA-101,매출세액,Y25017,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(광터교차로) BIM 설계,국도42 원주~흥업,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(삼안),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)진화기술공사,"13,200,000*10%",0,"1,320,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,홍아름,CEL회의 후 점심식대 (7명),0,"100,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,홍아름,CEL회의 후 점심식대 (7명),"100,000",0,"100,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,김윤재,"법인명변경/법인분할 등기 관련 서류 발급 수수료(인감증명서 4, 등본 2, 초본 2)",0,"4,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-20,1월,10115301,AST-106,매입세액,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 ERP서버 인터넷 회선 통신비 26년 01월 결제의 건,"51,000",0,"51,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),OBOBOB,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),12월 휴대폰 사용요금(최대선),0,"144,280",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,60114501,WF-301,도서인쇄비(신문및정기간행물),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,매일경제신문사(매경이코노미),매일경제 구독료(26년 1월),"25,000",0,"25,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115798,WF-106,복리후생비(공통),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,1월 총괄기획실 물품 및 다과 구매,"162,220",0,"162,220",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,60114398,OP-203,소모품비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,인프라 BIM1팀,안효원,GNSS 측량 테스트 안테나 구매,"300,500",0,"300,500",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"43,340",0,"43,340",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"43,340",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"28,350",0,"28,350",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"28,350",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-01-30,1월,10111101,REV-101,용역미수금(계산서발행),Y25017,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(광터교차로) BIM 설계,국도42 원주~흥업,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(삼안),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)진화기술공사,국도42호선원주흥업사제외1개소교차로개선공사실시설계용역(광터교차로)BIM설,0,"14,520,000",0,0,,수입,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-01-30,1월,10110501,AST-101,보통예금,Y25017,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(광터교차로) BIM 설계,국도42 원주~흥업,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(삼안),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,국민(주거래),국도42호선원주흥업사제외1개소교차로개선공사실시설계용역(광터교차로)BIM설,"14,520,000",0,0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-02,1월,20110101,LIA-101,외상매입금,X25055,GIS장비 개발,GIS 장비개발,GIS장비 개발,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,,엔비텍,SatPointer 하드웨어 개발,0,"8,800,000",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2026-01-20,2026-01-20,1월,50151309,OP-203,원가)소모품비(안전관리장비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,마천사무실/페이퍼타올 구매(25년 12월),"199,100",,"199,100",0,X,구매,일반,,,,,,,,,
|
||||
삼안,2026-01-20,2026-01-20,1월,60114733,IT-301,경상시험연구비(지급수수료),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,드론배상책임보험 환급금 과다지급에 따른 차액 지급의 건,"101,000",,"101,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,12월 유류비,0,"125,000",0,0,공통 → 그룹장,제외,기타,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,60115903,WF-201,여비교통비(국내출장비),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,12월 유류비,"125,000",0,"125,000",0,공통 → 그룹장,출장비,기타,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,인프라 BIM1팀,안효원,GNSS 측량 테스트 안테나 구매,0,"300,500",0,0,,제외,일반,,,,,,,,,
|
||||
삼안,2026-01-28,2026-01-27,1월,60114739,IT-201,경상시험연구비(소모품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,기술개발센터/신사무실 커피원두 구매(1월_정기구매),"660,000",,"660,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,업무회의 후 식대,0,"185,300",0,0,공통 → 그룹장,제외,기타,,,,,,,,,
|
||||
바론,2025-12-31,2026-01-02,1월,60115705,WF-101,복리후생비(회식대),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,업무회의 후 식대,"185,300",0,"185,300",0,공통 → 그룹장,복리후생비,기타,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 스토리지 12월 청구금액,0,"19,474",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-02,1월,60116399,IT-301,지급수수료(기타),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,류원준,"노션 구독료(25.12~25.12, 사내가이드)","187,420",0,"187,420",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,20111103,LIA-101,미지급금(일반미지급),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,통신비,0,"35,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-22,1월,60117301,WF-301,교육훈련비,X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,주식회사 케이지에듀원,바론컨설턴트 2025년 법정필수 및 산업안전보건교육,"1,332,000",0,"1,332,000",0,외주,외주,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,,비매출,S/W 개발,도로,기술개발센터,Infra Solution 개발팀,류한솔,InfraSolution 회식비,0,"499,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,60115705,WF-101,복리후생비(회식대),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,개발,비매출,S/W 개발,도로,기술개발센터,Infra Solution 개발팀,류한솔,InfraSolution 회식비,"499,000",0,"499,000",0,X,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25032,단가/공정 solution,단가/공정 solution,단가/공정 solution,S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,C.C.팀,정요한,팀 회식비,0,"333,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-07,1월,60115705,WF-101,복리후생비(회식대),X25032,단가/공정 solution,단가/공정 solution,단가/공정 solution,S/W개발,솔루션,기획,비매출,S/W 개발,솔루션,기술개발센터,C.C.팀,정요한,Construction Control 팀 회식비,"333,000",0,"333,000",0,X,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-17,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),25061,인천발 KTX 직결교량 시공,인천발 KTX 시공,인천발 KTX 직결교량 시공,직접매출,시공,,매출,가족사 프로젝트,시공,기술개발센터,프리팹 디비전장,심영표,출장비,"98,240",0,"98,240",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-17,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),25061,인천발 KTX 직결교량 시공,인천발 KTX 시공,인천발 KTX 직결교량 시공,직접매출,시공,,매출,가족사 프로젝트,시공,기술개발센터,프리팹 디비전장,심영표,출장비 - 심영표,0,"98,240",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25010,WayDraw,WayDraw,WayDraw,S/W개발,도로,,비매출,S/W 개발,도로,기술개발센터,Infra Solution 디비전장,김정훈,4분기 세미나 후 회식,0,"505,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,60115705,WF-101,복리후생비(회식대),X25010,WayDraw,WayDraw,WayDraw,S/W개발,도로,기획,비매출,S/W 개발,도로,기술개발센터,Infra Solution 디비전장,김정훈,4분기 세미나 후 회식,"505,000",0,"505,000",0,X,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),25061,인천발 KTX 직결교량 시공,인천발 KTX 시공,인천발 KTX 직결교량 시공,직접매출,시공,,매출,가족사 프로젝트,시공,기술개발센터,실물실증팀,이수문,출장비,"1,089,770",0,"1,089,770",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),25061,인천발 KTX 직결교량 시공,인천발 KTX 시공,인천발 KTX 직결교량 시공,직접매출,시공,,매출,가족사 프로젝트,시공,기술개발센터,실물실증팀,이수문,출장비 - 이수문,0,"1,089,770",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25030,GSIM,GSIM,GSIM,S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,팀 회식비,0,"313,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-07,1월,60115705,WF-101,복리후생비(회식대),X25030,GSIM,GSIM,GSIM,S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,GSIM개발팀 회식비,"313,200",0,"313,200",0,X,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,인프라 BIM1팀,김이훈,출장비,"425,620",0,"425,620",0,,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,인프라 BIM1팀,안효원,출장비 - 안효원,0,"240,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,인프라 BIM1팀,김이훈,출장비 - 김이훈,0,"185,620",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,한지아,Cursor AI Agent 사용료(디자인퍼블리싱),0,"137,666",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,50153199,IT-301,원가)지급수수료(기타),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,한지아,Cursor AI Agent 사용료(디자인퍼블리싱),"137,666",0,"137,666",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25021,LifeLine-Water,LifeLine-Water,LifeLine-Water,S/W개발,수리/수문,,비매출,S/W 개발,수리/수문,기술개발센터,상하수도팀,박현수,팀 세미나 회의비,0,"42,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-07,1월,50154701,WF-103,원가)회의비,X25021,LifeLine-Water,LifeLine-Water,LifeLine-Water,S/W개발,수리/수문,기획,비매출,S/W 개발,수리/수문,기술개발센터,상하수도팀,박현수,물관리&단지설계개발 자체 세미나 회의비,"42,200",0,"42,200",0,X,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25022,강우강도산정 프로그램,강우강도산정 프로그램,강우강도산정 프로그램,S/W개발,수리/수문,,비매출,S/W 개발,수리/수문,기술개발센터,수자원팀,이은구,수자원팀 회식비,0,"95,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-07,1월,60115705,WF-101,복리후생비(회식대),X25022,강우강도산정 프로그램,강우강도산정 프로그램,강우강도산정 프로그램,S/W개발,수리/수문,기획,비매출,S/W 개발,수리/수문,기술개발센터,수자원팀,이은구,꼬꼬잭치킨 거여마천점 수자원팀 회식비,"95,000",0,"95,000",0,X,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,60115903,WF-201,여비교통비(국내출장비),Z25058,서산~명천 도로건설공사 기본 및 실시설계,서산~명천 기본 및 실시설계,서산~명천 도로건설공사 기본 및 실시설계,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이배승,출장비,"96,264",0,"96,264",0,,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-23,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),Z25058,서산~명천 도로건설공사 기본 및 실시설계,서산~명천 기본 및 실시설계,서산~명천 도로건설공사 기본 및 실시설계,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이배승,출장비 - 이배승,0,"96,264",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-07,1월,20111103,LIA-101,미지급금(일반미지급),X25024,HmEG(HmDraw),HmEG(HmDraw),HmEG(HmDraw),S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,백승민,셀 회식비,0,"201,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-26,2026-01-07,1월,60115705,WF-101,복리후생비(회식대),X25024,HmEG(HmDraw),HmEG(HmDraw),HmEG(HmDraw),S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,백승민,HmEG셀 회식비,"201,000",0,"201,000",0,X,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-09,1월,20111103,LIA-101,미지급금(일반미지급),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 서버 12월 사용 금액,0,"290,556",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-29,1월,60116399,IT-301,지급수수료(기타),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,굿어스데이터 주식회사,센터 가상화서비스 12월 서비스 통합,"362,300",0,"362,300",0,,구매,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-09,1월,60115903,WF-201,여비교통비(국내출장비),Y25008,계양-강화간 고속도로 건설공사 기본설계단계 교량 BIM 설계(제5공구),계양~강화 고속도로(5공구),계양-강화 고속도로 건설 기본 및 실시(5공구),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM2팀,표종진,출장비,"142,310",0,"142,310",0,X,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-30,2026-01-09,1월,20111103,LIA-101,미지급금(일반미지급),Y25008,계양-강화간 고속도로 건설공사 기본설계단계 교량 BIM 설계(제5공구),계양~강화 고속도로(5공구),계양-강화 고속도로 건설 기본 및 실시(5공구),직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM2팀,표종진,출장비 - 표종진,0,"142,310",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2025-12-05,2026-01-09,1월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이광태,출장비,"60,000",0,"60,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-05,2026-01-09,1월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이광태,출장비,0,"60,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115903,WF-201,여비교통비(국내출장비),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,차량유류대/업무협의,"397,000",0,"397,000",0,HSJ,출장비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,차량유류대/업무협의,0,"397,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115903,WF-201,여비교통비(국내출장비),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,차량유류대/업무협의,"327,000",0,"327,000",0,HSJ,출장비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,차량유류대/업무협의,0,"327,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2025-12-22,2026-01-15,1월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이광태,출장비,"8,000",0,"8,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-22,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이광태,출장비 - 이광태,0,"8,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이광태,출장비,"16,000",0,"16,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,인프라 BIM1팀,이광태,출장비,0,"16,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,그룹장 업무협의 식대,"267,000",0,"267,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,그룹장 업무협의 식대,0,"267,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,Infra Solution팀 과업회의 식대,"283,800",0,"283,800",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,Infra Solution팀 과업회의 식대,0,"283,800",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,엔지니어링 개발그룹 성과회의 식대,"485,000",0,"485,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,엔지니어링 개발그룹 성과회의 식대,0,"485,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,Modeler셀 업무협의 식대,"309,000",0,"309,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,Modeler셀 업무협의 식대,0,"309,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115903,WF-201,여비교통비(국내출장비),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,차량유류대/업무협의,"334,000",0,"334,000",0,HSJ,출장비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,차량유류대/업무협의,0,"334,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,Strana팀 업무협의 식대,"284,800",0,"284,800",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,Strana팀 업무협의 식대,0,"284,800",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,EG-BIM 셀 팀회식,"266,000",0,"266,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,EG-BIM 셀 팀회식,0,"266,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,60115903,WF-201,여비교통비(국내출장비),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,한형관,차량유류대/업무협의,"338,000",0,"338,000",0,HSJ,출장비,기타,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-15,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,한형관,차량유류대/업무협의,0,"338,000",0,0,,제외,공통,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 개발그룹장,장계석,업무회의 간 식대 및 음료대,0,"88,100",0,0,공통 → 그룹장,제외,기타,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,50152507,WF-101,원가)복리후생비(회식대),X25002,KNGIL,KNGIL,KNGIL,S/W개발,기초조사및GIS,개발,비매출,S/W 개발,기초조사 및 GIS,기술개발센터,엔지니어링 개발그룹장,장계석,통합인증(회원관리) 회의 간 식대,"34,700",0,"34,700",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,50152507,WF-101,원가)복리후생비(회식대),X25002,KNGIL,KNGIL,KNGIL,S/W개발,기초조사및GIS,개발,비매출,S/W 개발,기초조사 및 GIS,기술개발센터,엔지니어링 개발그룹장,장계석,통합인증(회원관리) 회의 간 음료대,"9,500",0,"9,500",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,50152507,WF-101,원가)복리후생비(회식대),X25029,bCMf,bCMf,bCMf,S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,엔지니어링 개발그룹장,장계석,BCMF 회의 후 식사,"20,000",0,"20,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,50152507,WF-101,원가)복리후생비(회식대),X25042,ERP: 바론,ERP:바론,ERP: 바론,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,엔지니어링 개발그룹장,장계석,산하종합기술 미팅 간 식대,"6,500",0,"6,500",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,50152507,WF-101,원가)복리후생비(회식대),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,저가형 GNSS 계약 간 음료대,"17,400",0,"17,400",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2025-12-22,2026-01-20,1월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,구조물계획팀,김일태,출장비,"16,500",0,"16,500",0,,출장비,일반,,,,,,,,,
|
||||
바론,2025-12-22,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,구조물계획팀,김일태,출장비 - 김일태,0,"16,500",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-13,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,Infra Solution 개발팀,최정우,서류 발급수수료,0,"47,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-29,1월,10115301,AST-106,매입세액,X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,굿어스데이터 주식회사,"362,300*10%","36,230",0,"36,230",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-20,1월,20111103,LIA-101,미지급금(일반미지급),X25012,WallZainer,WallZainer,WallZainer,S/W개발,구조,,비매출,S/W 개발,구조물,기술개발센터,구조물 S/W개발팀,정계완,구조물SW개발팀 회식비,0,"103,600",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-07,2026-01-20,1월,60115705,WF-101,복리후생비(회식대),X25012,WallZainer,WallZainer,WallZainer,S/W개발,구조,개발,비매출,S/W 개발,구조물,기술개발센터,구조물 S/W개발팀,정계완,구조물SW개발팀 회식비,"103,600",0,"103,600",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-23,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 개발그룹장,장계석,업무회의 간 식대 및 음료대,0,"43,000",0,0,공통 → 그룹장,제외,기타,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-23,1월,50152507,WF-101,원가)복리후생비(회식대),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 개발그룹장,장계석,필리핀 지사장 업무회의 음료대,"5,000",0,"5,000",0,공통 → 그룹장,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-23,1월,50152507,WF-101,원가)복리후생비(회식대),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 개발그룹장,장계석,업무회의 간 식사,"32,000",0,"32,000",0,공통 → 그룹장,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-23,1월,50152507,WF-101,원가)복리후생비(회식대),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 개발그룹장,장계석,업무회의 간 음료대,"6,000",0,"6,000",0,공통 → 그룹장,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25053,수자원 해외사업,수자원 해외사업,수자원 해외사업,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,수자원팀,이은구,수자원팀 인원 충원후 회식,0,"147,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X25053,수자원 해외사업,수자원 해외사업,수자원 해외사업,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,수자원팀,이은구,제일수산 수자원팀 회식비,"147,000",0,"147,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25038,ERP: PTC,ERP:PTC,ERP: PTC,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,김윤하,PTC안전 업무인수인계 후 식사,0,"36,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X25038,ERP: PTC,ERP:PTC,ERP: PTC,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,김윤하,PTC안전 업무인수인계 후 식사,"36,000",0,"36,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25029,bCMf,bCMf,bCMf,S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,Web Solution팀,정명준,셀 회식,0,"65,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X25029,bCMf,bCMf,bCMf,S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,Web Solution팀,정명준,BCMF 업무인수 인계 후 식사,"65,000",0,"65,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25009,WayConfirm,WayConfirm,WayConfirm,S/W개발,도로,,비매출,S/W 개발,도로,기술개발센터,Infra Solution 개발팀,이가연,인프라솔루션 비탈면셀 회식비,0,"120,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X25009,WayConfirm,WayConfirm,WayConfirm,S/W개발,도로,개발,비매출,S/W 개발,도로,기술개발센터,Infra Solution 개발팀,이가연,인프라솔루션 비탈면셀 회식비,"120,000",0,"120,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25033,WatchBIM,WatchBIM,WatchBIM,S/W개발,도로,,비매출,S/W 개발,솔루션,기술개발센터,Infra Solution 개발팀,김재현,WatchBIM 셀 회식,0,"140,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-27,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X25033,WatchBIM,WatchBIM,WatchBIM,S/W개발,도로,개발,비매출,S/W 개발,솔루션,기술개발센터,Infra Solution 개발팀,김재현,WatchBIM 셀 회식,"140,000",0,"140,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),Y25004,"가평군하수관망계측시스템유지관리용역(프로그램 개발, 중앙제어실 및 서버유지관리)",가평군 하수관망시스템,가평군 하수관망 계측시스템 유지관리 용역,직접매출,콘텐츠제작,,매출,바론계약,기술용역,기술개발센터,Web Solution팀,이병권,출장비,"158,000",0,"158,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),Y25004,"가평군하수관망계측시스템유지관리용역(프로그램 개발, 중앙제어실 및 서버유지관리)",가평군 하수관망시스템,가평군 하수관망 계측시스템 유지관리 용역,직접매출,콘텐츠제작,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,출장비 - 장계석,0,"36,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),Y25004,"가평군하수관망계측시스템유지관리용역(프로그램 개발, 중앙제어실 및 서버유지관리)",가평군 하수관망시스템,가평군 하수관망 계측시스템 유지관리 용역,직접매출,콘텐츠제작,,매출,바론계약,기술용역,기술개발센터,Web Solution팀,신지호,출장비 - 신지호,0,"113,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),Y25004,"가평군하수관망계측시스템유지관리용역(프로그램 개발, 중앙제어실 및 서버유지관리)",가평군 하수관망시스템,가평군 하수관망 계측시스템 유지관리 용역,직접매출,콘텐츠제작,,매출,바론계약,기술용역,기술개발센터,Web Solution팀,이병권,출장비 - 이병권,0,"9,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 스토리지 01월 청구금액,0,"23,705",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2025-12-22,2026-01-08,1월,50153103,OUT-102,원가)지급수수료(용역수수료),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,,방화벽 유지보수 12월,"55,000",,"55,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25030,GSIM,GSIM,GSIM,S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,통신비,0,"26,110",0,0,,제외,일반,,,,,,,,,
|
||||
한맥,2025-12-23,2026-01-08,1월,50153103,OUT-102,원가)지급수수료(용역수수료),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,,통합 유지보수(서버외) 12월,"770,000",,"770,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25044,PQ시스템,PQ시스템,PQ시스템,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,신지호,PQ프로그램 개선 회의 음료,0,"10,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115705,WF-101,복리후생비(회식대),X25044,PQ시스템,PQ시스템,PQ시스템,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,신지호,PQ프로그램 개선 회의 음료,"10,000",0,"10,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-21,1월,50153199,IT-301,원가)지급수수료(기타),X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,구글클라우드 Ai 사용료(12월),"105,113",0,"105,113",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-21,1월,10115301,AST-106,매입세액,X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,구글클라우드 Ai 사용료(12월),"10,511",0,"10,511",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-19,2026-01-29,1월,20110101,LIA-101,외상매입금,X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,굿어스데이터 주식회사,센터 가상화서비스 12월 서비스 통합,0,"398,530",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,대전 kickoff 미팅,0,"47,400",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,60115903,WF-201,여비교통비(국내출장비),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,대전 kickoff 미팅 간 교통비,"47,400",0,"47,400",0,,출장비,R&D,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 개발그룹장,장계석,업무미팅 간 식대,0,"111,600",0,0,공통 → 그룹장,제외,기타,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,대전 kickoff 미팅 간 음료대,"11,700",0,"11,700",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,ETRI 회의 및 식사,"47,100",0,"47,100",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,ETRI 회의 및 식사,"15,500",0,"15,500",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,ETRI 회의 및 식사,"32,300",0,"32,300",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,50152507,WF-101,원가)복리후생비(회식대),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 개발그룹장,장계석,유지보수 업체 미팅,"5,000",0,"5,000",0,공통 → 그룹장,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,50153199,IT-301,원가)지급수수료(기타),X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,구글클라우드 Ai 사용료(11월),535,0,535,0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,10115301,AST-106,매입세액,X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,구글클라우드 Ai 사용료(11월),54,0,54,0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-01-21,1월,50153199,IT-301,원가)지급수수료(기타),X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,Cloudflare Inc.,Ai셀 Cloudflare 사용료(12월),"8,375",0,"8,375",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,50153199,IT-301,원가)지급수수료(기타),X25043,CivilEngineeringLab,CivilEngineeringLab,CivilEngineeringLab,기획/관리,기획&관리,,비매출,S/W 개발,운영S/W,총괄기획실,ERP기획팀,송대일,rightcivilengineering 도메인 구입(3년),"68,970",0,"68,970",0,,구매,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,50151301,OP-203,원가)소모품비(사무용품비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 설치용 USB 오피스 파우치 구매(2개),"31,700",0,"31,700",0,X,구매,일반,,,,,,,,,
|
||||
바론,2025-12-24,2026-01-02,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,스타벅스 기프트카드 구매,"1,000,000",0,"1,000,000",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/07),"59,100",0,"59,100",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-01-20,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,스타벅스 기프트카드 구매,"700,000",0,"700,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-09,2026-01-20,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,개발소프트웨어 영업(01/09),"45,400",0,"45,400",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-12,2026-01-20,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/09),"52,300",0,"52,300",0,,구매,일반,,,,,,,,,
|
||||
한맥,2026-01-13,2026-01-14,1월,60114744,WF-103,경상시험연구비(회의비(연구)),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,총괄기획실,,,스마트건설10/회의 후 식대(12/02),"87,000",,"87,000",0,,복리후생비,R&D,,,,,,,,,
|
||||
한맥,2026-01-13,2026-01-14,1월,60114744,WF-103,경상시험연구비(회의비(연구)),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,총괄기획실,,,스마트건설10/회의 후 식대(12/23),"355,000",,"355,000",0,,복리후생비,R&D,,,,,,,,,
|
||||
한맥,2026-01-13,2026-01-14,1월,60114744,WF-103,경상시험연구비(회의비(연구)),Y23113,실도로 기반 Lv4. 자율주행차량 운전능력 평가기술 개발,,,,,,,,,총괄기획실,,,실도로기반LV.4/회의 후 식대(12/12),"108,000",,"108,000",0,,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-13,2026-01-14,1월,60114744,WF-103,경상시험연구비(회의비(연구)),Y23113,실도로 기반 Lv4. 자율주행차량 운전능력 평가기술 개발,,,,,,,,,총괄기획실,,,실도로기반LV.4/회의 후 식대(12/16),"218,000",,"218,000",0,,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-13,2026-01-14,1월,60114744,WF-103,경상시험연구비(회의비(연구)),Y23113,실도로 기반 Lv4. 자율주행차량 운전능력 평가기술 개발,,,,,,,,,총괄기획실,,,실도로기반LV.4/회의 후 식대(12/23),"355,000",,"355,000",0,,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-16,2026-01-20,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/15),"22,200",0,"22,200",0,,구매,일반,,,,,,,,,
|
||||
한맥,2026-01-15,2026-01-15,1월,10113701,AST-104,선급금(일반),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,총괄기획실,,,스마트건설10세부/부가세 환입(12월),"327,444",,0,"327,444",,수입,R&D,,,,,,,,,
|
||||
한맥,2026-01-15,2026-01-15,1월,10113701,AST-104,선급금(일반),Y23113,실도로 기반 Lv4. 자율주행차량 운전능력 평가기술 개발,,,,,,,,,총괄기획실,,,실도로기반LV.4/부가세 환입(10~12월),"132,230",,0,"132,230",,수입,기타,,,,,,,,,
|
||||
한맥,2026-01-20,2026-01-20,1월,50152705,WF-201,원가)여비교통비(시내교통비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,마천사무실/11월 주차정기권 일할계산분,0,,0,0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-15,2026-01-20,1월,50153199,IT-301,원가)지급수수료(기타),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,총괄기획실,ERP기획팀,송대일,이지빔 도메인 연장 2건 (3년),"141,020",0,"141,020",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-20,1월,10352101,IT-101,소프트웨어,X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,총괄기획실,,The Intellicad Technology Consortium,EG-BIM 개발 및 사용을 위한 ITC (IntelliCAD) 로열티 지급의 건(외화),"51,569,000",0,"51,569,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-23,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,노트북가방 구매(01/18),"100,000",0,"100,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-20,2026-01-29,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/15),"9,000",0,"9,000",0,,구매,일반,,,,,,,,,
|
||||
한맥,2026-01-05,2026-01-14,1월,60115305,MK-201,접대비(경조금),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,,이종훈 아들결혼/26.1.11 일요일 12:30/영등포구 국회대로 612 더베르지,"500,000",,"500,000",0,"공통 → 교육훈련,참석",복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,지방도647호 대호대교/현장점검 후 회식,"283,000",,"283,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,모징지구 등 3개 지구단위계획 업무협의,"266,200",,"266,200",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,서산-명천 도로공사 팀회식,"65,000",,"65,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,새대천항 북방파제 부서회의,"265,200",,"265,200",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,서울남현 공공주택지구 현장점검,"284,000",,"284,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,2023년 아산 도시관리계획 업무협의,"241,000",,"241,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,용산국제업무지구 현장조사,"216,000",,"216,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,천안 에코밸리일반산단 진입도로 현장조사,"212,000",,"212,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,연산-두마 감리현장 업무협의,"153,000",,"153,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-09,2026-01-15,1월,60115705,WF-101,복리후생비(회식대),HSJHSJ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,새만금권역(지방하천) 기본기획 업무협의,"223,000",,"223,000",0,HSJ,복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-13,2026-01-15,1월,60115305,MK-201,접대비(경조금),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,,삼보 엄인섭사장 빙모상(전주삼성장례문화원 301호),"100,000",,"100,000",0,"공통 → 교육훈련,참석",복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-01-22,2026-01-23,1월,60115305,MK-201,접대비(경조금),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,,임명기 경조금,"100,000",,"100,000",0,"공통 → 교육훈련,참석",복리후생비,기타,,,,,,,,,
|
||||
삼안,2026-01-09,2026-01-09,1월,20111539,LIA-102,예수금(연구비),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,스마트건설10세부 연구비 부가세 환급,"469,000",,0,"469,000",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
삼안,2026-01-09,2026-01-09,1월,20111539,LIA-102,예수금(연구비),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,디지털1핵심/연구비 부가세 환급(10월~12월),"682,394",,0,"682,394",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
삼안,2026-01-09,2026-01-09,1월,20111539,LIA-102,예수금(연구비),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,2핵심/연구비 부가세 환급(10월~12월),"300,000",,0,"300,000",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
삼안,2026-01-19,2026-01-19,1월,20111539,LIA-102,예수금(연구비),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,스마트건설10세부 연구비 부가세 환급,"42,454",,0,"42,454",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
삼안,2026-01-19,2026-01-19,1월,20111539,LIA-102,예수금(연구비),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,디지털1핵심/연구비 부가세 환급(10월~12월),"48,380",,0,"48,380",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
바론,2026-01-22,2026-01-29,1월,50151301,OP-203,원가)소모품비(사무용품비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 홍보목적 브로슈어 클리어파일 구매(100매),"21,700",0,"21,700",0,,구매,일반,,,,,,,,,
|
||||
삼안,2026-01-22,2026-01-22,1월,20111539,LIA-102,예수금(연구비),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,스마트건설10세부 연구비 부가세 환급,"6,364",,0,"6,364",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
삼안,2026-01-22,2026-01-22,1월,20111539,LIA-102,예수금(연구비),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,디지털1핵심/연구비 부가세 환급(10월~12월),"45,411",,0,"45,411",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
삼안,2026-01-23,2026-01-23,1월,20111539,LIA-102,예수금(연구비),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,,디지털1핵심/연구비 부가세 환급(10월~12월),"3,545",,0,"3,545",부가세환급(수입)을 구분하기위해 LIA-102코드로 변경,수입,R&D,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,50153199,IT-301,원가)지급수수료(기타),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,총괄기획실,ERP기획팀,송대일,eg-bim 홈페이지 호스팅 업그레이드,"38,140",0,"38,140",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-23,2026-01-29,1월,50153199,IT-301,원가)지급수수료(기타),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,총괄기획실,ERP기획팀,송대일,egbim 홈페이지 트래픽 초기화,"2,750",0,"2,750",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-26,2026-01-29,1월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/09),"6,000",0,"6,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-02,1월,50170199,OUT-102,원가)기술협력비(기타외주비),X25055,GIS장비 개발,GIS 장비개발,GIS장비 개발,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,,엔비텍,SatPointer 하드웨어 개발,"8,000,000",0,"8,000,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-02,2026-01-02,1월,10115301,AST-106,매입세액,X25055,GIS장비 개발,GIS 장비개발,GIS장비 개발,기획/관리,기획&관리,,비매출,기획/제안,기획,기술개발센터,,엔비텍,SatPointer 하드웨어 개발,"800,000",0,"800,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-01-29,1월,50152999,OP-204,원가)통신비(기타),X25030,GSIM,GSIM,GSIM,S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,LTE 태블릿 통신비 청구(12월사용),"26,110",0,"26,110",0,,구매,일반,,,,,,,,,
|
||||
삼안,2026-01-28,2026-01-28,1월,60114739,IT-201,경상시험연구비(소모품비),X25024,HmEG(HmDraw),HmEG(HmDraw),HmEG(HmDraw),S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,기술개발센터,,,"(USD4,500*1,426.20)-소프트웨어 개발을 위한 ODA SDK 구매","6,417,900",,"6,417,900",0,X,구매,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-02-05,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,1월 유류비,0,"160,000",0,0,공통 → 그룹장,제외,기타,,,,,,,,,
|
||||
바론,2026-01-28,2026-02-05,2월,60115903,WF-201,여비교통비(국내출장비),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,1월 유류비,"160,000",0,"160,000",0,공통 → 그룹장,출장비,기타,,,,,,,,,
|
||||
바론,2026-01-28,2026-02-05,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,업무회의 후 식대,0,"151,000",0,0,공통 → 그룹장,제외,기타,,,,,,,,,
|
||||
바론,2026-01-28,2026-02-05,2월,60115705,WF-101,복리후생비(회식대),ZZZZZZ,공통(그룹장),공통(그룹장),공통(그룹장),기획/관리,공통,,공통,공통,공통,기술개발센터,엔지니어링 기획그룹장,양병홍,업무회의 후 식대,"151,000",0,"151,000",0,공통 → 그룹장,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-01-26,2026-02-05,2월,60115903,WF-201,여비교통비(국내출장비),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,이동원A,출장비,"76,800",0,"76,800",0,,출장비,R&D,,,,,,,,,
|
||||
바론,2026-01-26,2026-02-05,2월,20111103,LIA-101,미지급금(일반미지급),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,일반구조물 디비전장,이동원,출장비 - 이동원,0,"76,800",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,김민성,"AI LLM SubScription 12월, 1월",0,"625,388",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-06,2월,60116399,IT-301,지급수수료(기타),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,김민성,"AI LLM SubScription 12월, 1월","625,388",0,"625,388",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,,비매출,S/W 개발,도로,공통,Infra Solution 개발팀,장용섭,"WayDraw, WayPrimal 파트 회식",0,"192,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-06,2월,60115705,WF-101,복리후생비(회식대),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,개발,비매출,S/W 개발,도로,공통,Infra Solution 개발팀,장용섭,"항아리보쌈 WayDraw, WayPrimal 파트 회식","192,000",0,"192,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 서버 1월 사용 금액,0,"167,818",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-06,2월,50151399,OP-203,원가)소모품비(기타),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,프로젝트마스터 클라우드 서버 1월 사용 금액,"167,818",0,"167,818",0,계정코드 수정(50151309 확인불가),구매,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25027,StrAna,StrAna,StrAna,S/W개발,구조해석,,비매출,S/W 개발,그래픽&구조해석,기술개발센터,Strana 개발팀,이호경,팀 회식비,0,"150,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-06,2월,60115705,WF-101,복리후생비(회식대),X25027,StrAna,StrAna,StrAna,S/W개발,구조해석,개발,비매출,S/W 개발,그래픽&구조해석,기술개발센터,Strana 개발팀,이호경,Strana 개발팀 회식비,"150,000",0,"150,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25012,WallZainer,WallZainer,WallZainer,S/W개발,구조,,비매출,S/W 개발,구조물,기술개발센터,구조물 S/W개발팀,정계완,구조물SW개발팀 회식비,0,"161,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-06,2월,60115705,WF-101,복리후생비(회식대),X25012,WallZainer,WallZainer,WallZainer,S/W개발,구조,개발,비매출,S/W 개발,구조물,기술개발센터,구조물 S/W개발팀,정계완,구조물SW개발팀 회식비,"161,400",0,"161,400",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25002,KNGIL,KNGIL,KNGIL,S/W개발,기초조사및GIS,,비매출,S/W 개발,기초조사 및 GIS,기술개발센터,천지인팀,손원일,팀회식비,0,"161,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25002,KNGIL,KNGIL,KNGIL,S/W개발,기초조사및GIS,개발,비매출,S/W 개발,기초조사 및 GIS,기술개발센터,천지인팀,손원일,단지설계셀 회식비,"161,000",0,"161,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25009,WayConfirm,WayConfirm,WayConfirm,S/W개발,도로,,비매출,S/W 개발,도로,기술개발센터,인프라 BIM2팀,이동호,팀 회식비,0,"221,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25009,WayConfirm,WayConfirm,WayConfirm,S/W개발,도로,기획,비매출,S/W 개발,도로,기술개발센터,인프라 BIM2팀,이동호,BIM2팀 회식비,"221,000",0,"221,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,,이동원A,출장비,"88,520",0,"88,520",0,,출장비,R&D,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25057,디지털 국토정보 기술개발사업 1핵심과제(삼안),디지털 국토정보(1핵심),디지털 국토정보 기술개발사업,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,일반구조물 디비전장,이동원,,0,"88,520",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,(재)건설기술교육원,건설기술인승급교육,0,"200,000",0,0,시공BIM → 교육훈련,제외,기타,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,50153301,WF-301,원가)교육훈련비,ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,(재)건설기술교육원,건설기술인승급교육,"200,000",0,"200,000",0,시공BIM → 교육훈련,구매,기타,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM1팀,황은식,경부선 철도사업 측량분야 수행 회의 후 저녁식사,0,"119,000",0,0,영수증(수도권) → 팀 기준,제외,기타,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM1팀,황은식,경부선 철도사업 측량분야 수행 회의 후 저녁식사,"119,000",0,"119,000",0,영수증(수도권) → 팀 기준,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM1팀,황은식,경부선 드론측량 수행 관련 회의 후 식대,0,"35,500",0,0,영수증(수도권) → 팀 기준,제외,기타,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM1팀,황은식,경부선 드론측량 수행 관련 회의 후 식대,"35,500",0,"35,500",0,영수증(수도권) → 팀 기준,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,,비매출,S/W 개발,도로,기술개발센터,인프라 BIM1팀,황은식,BIM 1팀 Way셀 회식,0,"241,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,기획,비매출,S/W 개발,도로,기술개발센터,인프라 BIM1팀,황은식,BIM 1팀 Way셀 회식,"241,000",0,"241,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),Z25059,울산~외곽순환 고속도로(1공구),울산외곽순환(1공구),울산~외곽순환 고속도로(1공구),직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,터널팀,이화영,출장비,"33,200",0,"33,200",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),Z25059,울산~외곽순환 고속도로(1공구),울산외곽순환(1공구),울산~외곽순환 고속도로(1공구),직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,터널팀,이화영,출장비,0,"33,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,이병권,업무미팅 간 식대,0,"8,500",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,50152507,WF-101,원가)복리후생비(회식대),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,이병권,유지보수 업체 미팅,"8,500",0,"8,500",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,(재)건설기술교육원,건설기술인최초교육,0,"200,000",0,0,시공BIM → 교육훈련,제외,기타,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,50153301,WF-301,원가)교육훈련비,ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,(재)건설기술교육원,건설기술인최초교육,"200,000",0,"200,000",0,시공BIM → 교육훈련,구매,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,스마트건설팀,복진훈,스마트건설 사전점검 평가 후 회식,0,"505,000",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,스마트건설팀,복진훈,스마트건설 사전점검 평가 후 회식,"505,000",0,"505,000",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,스마트건설팀,복진훈,10세부 최종평가 사전점검 회의 후 점심식대,0,"113,000",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25056,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,스마트건설팀,복진훈,10세부 최종평가 사전점검 회의 후 점심식대,"113,000",0,"113,000",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,,비매출,S/W 개발,도로,기술개발센터,인프라 BIM2팀,표종진,팀 회식비,0,"190,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25008,WayPrimal,WayPrimal,WayPrimal,S/W개발,도로,기획,비매출,S/W 개발,도로,기술개발센터,인프라 BIM2팀,표종진,BIM2팀 회식비,"190,000",0,"190,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25025,EG-BIM Modeler,EG-BIM Modeler,EG-BIM Modeler,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,김민성,팀장 회식비,0,"100,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25025,EG-BIM Modeler,EG-BIM Modeler,EG-BIM Modeler,S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,김민성,그래픽스개발팀 셀장 회식비,"100,000",0,"100,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,이동원A,출장비,"172,710",0,"172,710",0,사전기획 → 경부선(평택-직지사),출장비,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,일반구조물 디비전장,이동원,출장비 - 이동원,0,"172,710",0,0,사전기획 → 경부선(평택-직지사),제외,기타,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,쓰리디랩스 드론 실증관련 업무협의,0,"61,800",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,쓰리디랩스 드론 실증관련 업무협의 간 식대,"61,800",0,"61,800",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-13,2월,20111103,LIA-101,미지급금(일반미지급),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,팀 회식비,0,"347,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-13,2월,60115705,WF-101,복리후생비(회식대),X25028,문서관리시스템(PM),문서관리시스템(PM),문서관리시스템(PM),S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,GSIM 개발팀,이호성,GSIM개발팀 회식비,"347,000",0,"347,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-13,2월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,하부구조팀,최창인,출장비,"146,828",0,"146,828",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-13,2월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,기술개발센터,하부구조팀,최창인,출장비 - 최창인,0,"146,828",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-20,2월,60116399,IT-301,지급수수료(기타),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,굿어스데이터 주식회사,센터 가상화서비스 01월 서비스 통합,"361,900",0,"361,900",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-20,2월,10115301,AST-106,매입세액,X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,굿어스데이터 주식회사,"361,900*10%","36,190",0,"36,190",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-20,2월,20110101,LIA-101,외상매입금,X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,,굿어스데이터 주식회사,센터 가상화서비스 01월 서비스 통합,0,"398,090",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,(재)건설기술교육원,건설기술인승급교육,0,"200,000",0,0,시공BIM → 교육훈련,제외,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,50153301,WF-301,원가)교육훈련비,ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,(재)건설기술교육원,건설기술인승급교육,"200,000",0,"200,000",0,시공BIM → 교육훈련,구매,기타,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,AbutZainer,AbutZainer,AbutZainer,S/W개발,구조,,비매출,S/W 개발,구조물,기술개발센터,하부구조팀,김승호,회식비 템플릿,0,"141,000",0,0,공통 → AbutZainer,제외,일반,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-20,2월,60115705,WF-101,복리후생비(회식대),ZZZZZZ,AbutZainer,AbutZainer,AbutZainer,S/W개발,구조,기획,비매출,S/W 개발,구조물,기술개발센터,하부구조팀,김승호,깡돈식당 하부구조팀 회식비,"141,000",0,"141,000",0,공통 → AbutZainer,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-20,2월,60115903,WF-201,여비교통비(국내출장비),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM1팀,김이훈,출장비,"147,490",0,"147,490",0,,출장비,기타,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,인프라 BIM1팀,김이훈,출장비 - 김이훈,0,"147,490",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),X25027,StrAna,StrAna,StrAna,S/W개발,구조해석,,비매출,S/W 개발,그래픽&구조해석,기술개발센터,Strana 개발팀,이호경,팀 회식비,0,"132,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-20,2월,60115705,WF-101,복리후생비(회식대),X25027,StrAna,StrAna,StrAna,S/W개발,구조해석,개발,비매출,S/W 개발,그래픽&구조해석,기술개발센터,Strana 개발팀,이호경,Strana 개발팀 회식비,"132,000",0,"132,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,구글클라우드 코리아 유한회사,경부선 드론촬영 원본데이터 보관 목적 웹하드 구매,0,"59,500",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-20,2월,60114398,OP-203,소모품비(기타),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,구글클라우드 코리아 유한회사,경부선 드론촬영 원본데이터 보관 목적 웹하드 구매,"59,500",0,"59,500",0,,구매,기타,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-25,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,경영지원부,,이용운,건강검진 비용 청구,0,"266,700",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-25,2월,60115798,WF-106,복리후생비(공통),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,경영지원부,,이용운,건강검진 비용 청구,"266,700",0,"266,700",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25033,WatchBIM,WatchBIM,WatchBIM,S/W개발,도로,,비매출,S/W 개발,솔루션,기술개발센터,Infra Solution 개발팀,김재현,셀 회식비,0,"112,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-27,2월,60115705,WF-101,복리후생비(회식대),X25033,WatchBIM,WatchBIM,WatchBIM,S/W개발,도로,개발,비매출,S/W 개발,솔루션,기술개발센터,Infra Solution 개발팀,김재현,셀 회식비,"112,000",0,"112,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25025,EG-BIM Modeler,EG-BIM Modeler,EG-BIM Modeler,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,이재원,팀 회식비,0,"164,500",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60115705,WF-101,복리후생비(회식대),X25025,EG-BIM Modeler,EG-BIM Modeler,EG-BIM Modeler,S/W개발,그래픽,개발,비매출,S/W 개발,그래픽&구조해석,기술개발센터,그래픽스 개발팀,이재원,그래픽스 개발팀 Modeler셀 회식비,"164,500",0,"164,500",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2025-12-15,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),Y26017,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,출장 간 하이패스 비용,0,"26,500",0,0,가평+경부선 출장비 합산 청구 → 가평 근무기록(1/30) → 경부선 통합 청구,제외,기타,,,,,,,,,
|
||||
바론,2025-12-15,2026-02-27,2월,60115903,WF-201,여비교통비(국내출장비),Y26017,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,출장 간 하이패스 비용,"26,500",0,"26,500",0,가평+경부선 출장비 합산 청구 → 가평 근무기록(1/30) → 경부선 통합 청구,출장비,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),Y26017,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,철도 드론 촬영 간 식대 및 음료대,0,"176,000",0,0,영수증 내역(충북) → 직지사,제외,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),Y26017,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,철도 드론 촬영 간 식대,"2,000",0,"2,000",0,영수증 내역(충북) → 직지사,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),Y26017,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,철도 드론 촬영 간 식대,"81,000",0,"81,000",0,영수증 내역(충북) → 직지사,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),Y26017,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,철도 드론 촬영 간 식대,"24,000",0,"24,000",0,영수증 내역(충북) → 직지사,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),Y26017,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,엔지니어링 개발그룹장,장계석,철도 드론 촬영 간 식대,"69,000",0,"69,000",0,영수증 내역(충북) → 직지사,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,국토정보 2핵심 국토부 보고 간 교통비,0,"120,330",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60115903,WF-201,여비교통비(국내출장비),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,국토정보 2핵심 국토부 보고 간 교통비,"120,330",0,"120,330",0,,출장비,R&D,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,국토정보 국토부 보고 간 식대 및 음료대,0,"36,800",0,0,,제외,R&D,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,국토정보 국토부 보고 간 식대,"10,000",0,"10,000",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,국토정보 국토부 보고 간 음료대,"17,500",0,"17,500",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),X25058,디지털 국토정보 기술개발사업 2핵심과제(삼안),디지털 국토정보(2핵심),국토정보 2핵심,직접매출,R&D,,매출,가족사 프로젝트,R&D,기술개발센터,엔지니어링 개발그룹장,장계석,국토정보 국토부 보고 간 음료대,"9,300",0,"9,300",0,,복리후생비,R&D,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,신지호,전산 유지보수 신규 업체 미팅 간 음료대,0,"7,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,50152507,WF-101,원가)복리후생비(회식대),X25046,전산운영관리,전산운영관리,전산운영관리,기획/관리,운영 S/W,,비매출,S/W 개발,운영S/W,기술개발센터,Web Solution팀,신지호,전산 유지보수 신규 업체 미팅 간 음료대,"7,200",0,"7,200",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25012,WallZainer,WallZainer,WallZainer,S/W개발,구조,,비매출,S/W 개발,구조물,기술개발센터,구조물 S/W개발팀,김세열,AI LLM 구독(초과분 API) 25년9월 ~ 26년1월 사용분,0,"522,590",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60116399,IT-301,지급수수료(기타),X25012,WallZainer,WallZainer,WallZainer,S/W개발,구조,개발,비매출,S/W 개발,구조물,기술개발센터,구조물 S/W개발팀,김세열,AI LLM 구독(초과분 API) 25년9월 ~ 26년1월 사용분,"522,590",0,"522,590",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25029,bCMf,bCMf,bCMf,S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,Web Solution팀,김윤하,사업단 BCMF시연준비후 식사,0,"179,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,60115705,WF-101,복리후생비(회식대),X25029,bCMf,bCMf,bCMf,S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,Web Solution팀,김윤하,사업단 BCMF시연준비후 식사,"179,000",0,"179,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-02,2월,20110101,LIA-101,외상매입금,X25051,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,(주)지케이측량시스템,측량장비(GNSS) CHCNAV i85 구매의 건,0,"5,500,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-02,2월,50151399,OP-203,원가)소모품비(기타),X25051,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,(주)지케이측량시스템,측량장비(GNSS) CHCNAV i85 구매의 건,"5,000,000",0,"5,000,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-02,2월,10115301,AST-106,매입세액,X25051,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,(주)지케이측량시스템,측량장비(GNSS) CHCNAV i85 구매의 건,"500,000",0,"500,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-04,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,월드번역통역,영문홍보물 제작을 위한 번역(가족사&바론브로슈어),0,"426,930",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-04,2월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,월드번역통역,영문홍보물 제작을 위한 번역(가족사&바론브로슈어),"388,119",0,"388,119",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-04,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,월드번역통역,"매입세액(388,119*0.1)","38,811",0,"38,811",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-05,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,협업증진팀,한승민,"1월 한맥 농구동호회 대관비 (2회분, 1/14, 28) / 대관장소 사정으로 2회분 동일 결제",0,"320,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-05,2월,60115798,WF-106,복리후생비(공통),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,협업증진팀,한승민,"1월 한맥 농구동호회 대관비 (2회분, 1/14, 28) / 대관장소 사정으로 2회분 동일 결제","320,000",0,"320,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-05,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,외부회의 음료,0,"35,100",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-05,2월,60115705,WF-101,복리후생비(회식대),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,외부회의 음료,"35,100",0,"35,100",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-05,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,외부식사 (건설기술인협회),0,"37,500",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-05,2월,60115705,WF-101,복리후생비(회식대),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,외부식사 (건설기술인협회),"37,500",0,"37,500",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-02-06,2월,60115903,WF-201,여비교통비(국내출장비),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,출장비,"8,000",0,"8,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-21,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,출장비 - 임민경,0,"8,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,김우진,(주)장헌산업 사업자등록 신청,0,"19,500",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-06,2월,60115903,WF-201,여비교통비(국내출장비),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,김우진,(주)장헌산업 사업자등록 신청,"19,500",0,"19,500",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-02-06,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"30,960",0,"30,960",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-28,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"30,960",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-06,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"113,850",0,"113,850",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,출장비 - 권혁진,0,"30,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-29,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"83,850",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/29),0,"65,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-06,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/29),"65,000",0,"65,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-02-06,2월,60115903,WF-201,여비교통비(국내출장비),X26006,업무/사업관리,업무/사업관리,업무/사업관리,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,출장비,"8,000",0,"8,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X26006,업무/사업관리,업무/사업관리,업무/사업관리,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,임민경,출장비 - 임민경,0,"8,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,1월 총괄기획실 물품 및 다과 구매(추가),0,"58,128",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-06,2월,60115798,WF-106,복리후생비(공통),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,1월 총괄기획실 물품 및 다과 구매(추가),"58,128",0,"58,128",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,26년 1월 4대보험료 (전체),0,"62,685,600",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,50152527,HR-204,원가)복리후생비(산재보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,26년 1월 4대보험료(산재보험/회사부담),"3,083,300",0,"3,083,300",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,50152521,HR-202,원가)복리후생비(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,26년 1월 4대보험료(건강보험/회사부담),"12,661,000",0,"12,661,000",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,20111505,HR-202,예수금(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,26년 1월 4대보험료(건강보험/직원부담),"12,661,000",0,"12,661,000",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,50152521,HR-202,원가)복리후생비(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,26년 1월 4대보험료(요양보험/회사부담),"1,663,310",0,"1,663,310",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,20111505,HR-202,예수금(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,26년 1월 4대보험료(요양보험),"1,663,310",0,"1,663,310",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,50151719,HR-201,원가)세금과공과(국민연금),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민연금관리공단,26년 1월 4대보험료(국민연금/회사부담),"12,096,220",0,"12,096,220",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,20111507,HR-201,예수금(국민연금),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민연금관리공단,26년 1월 4대보험료(국민연금/직원부담),"12,096,220",0,"12,096,220",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,50152529,HR-203,원가)복리후생비(고용보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,26년 1월 4대보험료(고용보험/회사부담),"3,808,750",0,"3,808,750",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-06,2월,20111509,HR-203,예수금(고용보험료(직원)),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,26년 1월 4대보험료(고용보험/직원부담),"2,952,490",0,"2,952,490",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-06,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 설치용 USB구매 (16GB*10개),0,"79,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-06,2월,60114301,OP-203,소모품비(사무용품비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 설치용 USB구매 (16GB*10개),"79,000",0,"79,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-09,2월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,임원실 소형테이블 모니터 설치 부가 물품 구매,0,"142,290",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-09,2월,60114305,IT-201,소모품비(전산용품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,미니PC 브라켓 구매,"16,500",0,"16,500",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-09,2월,60114305,IT-201,소모품비(전산용품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,멀티탭 보관함 구매,"16,790",0,"16,790",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-02,2026-02-09,2월,60114305,IT-201,소모품비(전산용품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,무선 마우스 구매(로지텍),"109,000",0,"109,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,경영및기술지원서비스(장헌산업),0,"28,270,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-10,2월,10110501,AST-101,보통예금,Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,국민(주거래),경영및기술지원서비스(장헌산업),"28,270,000",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,2026경영및기술지원서비스(장헌산업),0,"18,241,300",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10110501,AST-101,보통예금,Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(장헌산업),"18,241,300",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,2026경영및기술지원서비스(피티씨),0,"23,760,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10110501,AST-101,보통예금,Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(피티씨),"23,760,000",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-15,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y26014,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(진우30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,(주)진우엔지니어링,한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(진우30%)-인쇄/편집,"1,815,000",0,0,"1,815,000",,수입,기타,,,,,,,,,
|
||||
바론,2026-01-15,2026-02-10,2월,40110501,REV-101,개발수입,Y26014,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(진우30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,(주)진우엔지니어링,한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(진우30%)-인쇄/편집,0,"1,650,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-15,2026-02-10,2월,20112901,LIA-101,매출세액,Y26014,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(진우30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,(주)진우엔지니어링,"1,650,000*10%",0,"165,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,2026경영및기술지원서비스(장헌산업),"18,241,300",0,0,"18,241,300",,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,40110401,REV-101,관리용역수입,Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,2026경영및기술지원서비스(장헌산업),0,"16,583,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,20112901,LIA-101,매출세액,Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,"16,583,000*10%",0,"1,658,300",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,2026경영및기술지원서비스(피티씨),"23,760,000",0,0,"23,760,000",,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,40110401,REV-101,관리용역수입,Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,2026경영및기술지원서비스(피티씨),0,"21,600,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,20112901,LIA-101,매출세액,Y26010,2026 경영 및 기술지원 서비스(피티씨),,,,,,,,,총괄기획실,,피티씨,"21,600,000*10%",0,"2,160,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,2026경영및기술지원서비스(장헌산업),"18,241,300",0,0,"18,241,300",,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-02-10,2월,40110401,REV-101,관리용역수입,Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,2026경영및기술지원서비스(장헌산업),0,"16,583,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-01-13,2026-02-10,2월,20112901,LIA-101,매출세액,Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,"16,583,000*10%",0,"1,658,300",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10111101,REV-101,용역미수금(계산서발행),Y26014,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(진우30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,(주)진우엔지니어링,한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(진우30%)-인쇄/편집,0,"1,815,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-10,2월,10110501,AST-101,보통예금,Y26014,한강교량 보수보강공사 감독권한대행등 건설사업관리용역(2공구)(진우30%)-인쇄/편집,한강교량 사업관리(2공구),한강교량 사업관리(2공구),직접매출,콘텐츠제작,,매출,바론계약,콘텐츠 제작,기술개발센터,,국민(주거래),한강교량보수보강공사감독권한대행등건설사업관리용역(2공구)(진우30%)-인쇄/편집,"1,815,000",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2025-07-10,2026-02-11,2월,10111101,REV-101,용역미수금(계산서발행),Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,경영및기술지원서비스(장헌산업),"28,270,000",0,0,"28,270,000",,수입,기타,,,,,,,,,
|
||||
바론,2025-07-10,2026-02-11,2월,40110401,REV-101,관리용역수입,Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,경영및기술지원서비스(장헌산업),0,"25,700,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2025-07-10,2026-02-11,2월,20112901,LIA-101,매출세액,Y25039,경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,"25,700,000*10%",0,"2,570,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,기술기획팀,최현호,경조금 (최현호 선임연구원 자녀 출생),0,"300,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,50152511,WF-106,원가)복리후생비(경조금),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,기술기획팀,최현호,경조금 (최현호 선임연구원 자녀 출생),"300,000",0,"300,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,20111109,LIA-101,미지급금(하나카드),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,,하나카드(6669),SW 설치용 USB 대량 구매(4GB*100),0,"392,810",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,50151301,OP-203,원가)소모품비(사무용품비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 설치용 USB 대량 구매(4GB*100),"392,810",0,"392,810",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"67,020",0,"67,020",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-04,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"67,020",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사이승희사무소,바론 목적변경 취득세 및 기타 수수료,0,"67,240",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사이승희사무소,바론 목적변경 취득세 및 기타 수수료,"67,240",0,"67,240",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,홍보 전단지 제작,0,"1,232,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,60114599,WF-105,도서인쇄비(기타),X25006,GAIA,GAIA,GAIA,S/W개발,기초조사및GIS,영업,비매출,S/W 개발,기초조사 및 GIS,총괄기획실,,대한고주파,GAIA (홍보전단지 제작),"210,000",0,"210,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,"매입세액 (210,000*0.1)","21,000",0,"21,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,60114599,WF-105,도서인쇄비(기타),Y25013,iPipe 프로그램 구매 (CAD용),iPipe 프로그램 구매 (CAD용),iPipe 프로그램 구매 (CAD용),S/W개발,수리/수문,영업,비매출,S/W 개발,수리/수문,총괄기획실,,대한고주파,iPipeS (홍보전단지 제작),"210,000",0,"210,000",0,,구매,기타,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,"매입세액 (210,000*0.1)","21,000",0,"21,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,60114599,WF-105,도서인쇄비(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,디자인포트폴리오 (홍보전단지 제작),"70,000",0,"70,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,"매입세액 (70,000*0.1)","7,000",0,"7,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,60114599,WF-105,도서인쇄비(기타),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,,대한고주파,EG-BIM (홍보전단지 제작),"210,000",0,"210,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,"매입세액 (210,000*0.1)","21,000",0,"21,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,60114599,WF-105,도서인쇄비(기타),X25002,KNGIL,KNGIL,KNGIL,S/W개발,기초조사및GIS,영업,비매출,S/W 개발,기초조사 및 GIS,총괄기획실,,대한고주파,KNGIL (홍보전단지 제작),"210,000",0,"210,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,"매입세액 (210,000*0.1)","21,000",0,"21,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,60114599,WF-105,도서인쇄비(기타),X25020,TOVA,TOVA,TOVA,S/W개발,교통,영업,비매출,S/W 개발,교통,총괄기획실,,대한고주파,TOVQ (홍보전단지 제작),"210,000",0,"210,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,대한고주파,"매입세액 (210,000*0.1)","21,000",0,"21,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"58,020",0,"58,020",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-09,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"58,020",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"189,900",0,"189,900",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,0,"189,900",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/04),0,"10,300",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/04),"10,300",0,"10,300",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,20110101,LIA-101,외상매입금,Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)경수,경부선 평택-직지사구간 실시설계 측량 분야(외주착수금),0,"26,950,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,50170199,OUT-102,원가)기술협력비(기타외주비),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)경수,경부선 평택-직지사구간 실시설계 측량 분야(외주착수금),"24,500,000",0,"24,500,000",0,외주,외주,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,10115301,AST-106,매입세액,Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,(주)경수,경부선 평택-직지사구간 실시설계 측량 분야(외주착수금),"2,450,000",0,"2,450,000",0,외주,외주,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,20110101,LIA-101,외상매입금,Y26017,경부선 노량진-평택구간 작업자 이동통로 확보를 위한 기술조사 및 실시설계용역,경부선 (노량진-평택) 측량,경부선 노량진-평택구간 작업자 이동통로 확보를 위한 기술조사 및 실시설계용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,둠둠 주식회사,경부선 노량진~직지사구간 작업자 이동통로 확보 기술조사 및 실시설계용역(외주착수금),0,"25,575,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,50170199,OUT-102,원가)기술협력비(기타외주비),Y26017,경부선 노량진-평택구간 작업자 이동통로 확보를 위한 기술조사 및 실시설계용역,경부선 (노량진-평택) 측량,경부선 노량진-평택구간 작업자 이동통로 확보를 위한 기술조사 및 실시설계용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,둠둠 주식회사,경부선 노량진~직지사구간 작업자 이동통로 확보 기술조사 및 실시설계용역(외주착수금),"23,250,000",0,"23,250,000",0,외주,외주,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,10115301,AST-106,매입세액,Y26017,경부선 노량진-평택구간 작업자 이동통로 확보를 위한 기술조사 및 실시설계용역,경부선 (노량진-평택) 측량,경부선 노량진-평택구간 작업자 이동통로 확보를 위한 기술조사 및 실시설계용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,둠둠 주식회사,경부선 노량진~직지사구간 작업자 이동통로 확보 기술조사 및 실시설계용역(외주착수금),"2,325,000",0,"2,325,000",0,외주,외주,기타,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/30),0,"24,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-03,2026-02-12,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(01/30),"24,000",0,"24,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,20110101,LIA-101,외상매입금,Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,둠둠 주식회사,경부선 평택~직지사구간 작업자 이동통로 확보 기술조사 및 실시설계용역(외주착수금),0,"9,762,500",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,50170199,OUT-102,원가)기술협력비(기타외주비),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,둠둠 주식회사,경부선 평택~직지사구간 작업자 이동통로 확보 기술조사 및 실시설계용역(외주착수금),"8,875,000",0,"8,875,000",0,외주,외주,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-12,2월,10115301,AST-106,매입세액,Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,기술개발센터,,둠둠 주식회사,경부선 평택~직지사구간 작업자 이동통로 확보 기술조사 및 실시설계용역(외주착수금),"887,500",0,"887,500",0,외주,외주,기타,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,출장비,"79,760",0,"79,760",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-06,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,출장비 - 최근혜,0,"79,760",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,바론 입찰제출용 법인등기 발급 수수료,0,"10,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,바론 입찰제출용 법인등기 발급 수수료,"10,000",0,"10,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"48,640",0,"48,640",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"48,640",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,20110101,LIA-101,외상매입금,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 전화 통신비 26년 02월,0,"3,300",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,60116199,OP-204,통신비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 전화 통신비 26년 02월,"3,000",0,"3,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,10115301,AST-106,매입세액,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),공급가액*10%,300,0,300,0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사이승희사무소,바론 목적변경등기(법무사수수료),0,"253,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사이승희사무소,바론 목적변경등기(법무사수수료),"230,000",0,"230,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-12,2월,10115301,AST-106,매입세액,X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사이승희사무소,"매입세액(230,000*0.1)","23,000",0,"23,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,20111105,LIA-101,미지급금(KB국민카드),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민카드(3866),삼안 경영전략본부 회의 후 식사,0,"338,000",0,0,공통 → 인사교육,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,삼안 경영전략본부 회의 후 식사,"338,000",0,"338,000",0,공통 → 인사교육,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,권혁진,업무용 산업의 역군 서비스 이용료(1월),0,"99,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,권혁진,업무용 산업의 역군 서비스 이용료(1월),"99,000",0,"99,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,"기술기획팀 AI기획에 따른 AI결제(chatGPT, NotebookLM)",0,"61,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,60114398,OP-203,소모품비(기타),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,"기술기획팀 AI기획에 따른 AI결제(chatGPT, NotebookLM)","61,400",0,"61,400",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,기술기획팀 점심 회식비(최현호 출산휴가),0,"200,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,60115705,WF-101,복리후생비(회식대),X25051,사전기획,사전기획,사전기획,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,기술기획팀,김원기,기술기획팀 점심 회식비(최현호 출산휴가),"200,000",0,"200,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,이지빔 영업(02/11) 수원&강남 단기 렌트카,0,"40,430",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-12,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,이지빔 영업(02/11) 수원&강남 단기 렌트카,"40,430",0,"40,430",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-01-31,2026-02-12,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"87,460",0,"87,460",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-31,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"87,460",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,(주)장헌 브로슈어 당진 택배발송 비용,0,"12,100",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-05,2026-02-12,2월,50153199,IT-301,원가)지급수수료(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,솔루션통합팀,김지영A,(주)장헌 브로슈어 당진 택배발송 비용,"12,100",0,"12,100",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,20110101,LIA-101,외상매입금,Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,SK브로드밴드(주),서산-아산 건설사업단 BIG ROOM VPN 26년 02월 결제의 건,0,"94,600",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,60116199,OP-204,통신비(기타),Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,SK브로드밴드(주),서산-아산 건설사업단 BIG ROOM VPN 26년 02월 결제의 건,"86,000",0,"86,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,10115301,AST-106,매입세액,Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,SK브로드밴드(주),서산-아산 건설사업단 BIG ROOM VPN 26년 02월 결제의 건,"8,600",0,"8,600",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,외빈 선물 구매,0,"179,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,60114398,OP-203,소모품비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,외빈 선물 구매,"100,000",0,"100,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,60114398,OP-203,소모품비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,최근혜,외빈 선물 구매,"79,000",0,"79,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-13,2월,20111103,LIA-101,미지급금(일반미지급),X25035,Domainer,Domainer,Domainer,S/W개발,솔루션,,비매출,S/W 개발,솔루션,총괄기획실,협업증진팀,성형일,협업증진팀 식대,0,"112,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-13,2월,60115705,WF-101,복리후생비(회식대),X25035,Domainer,Domainer,Domainer,S/W개발,솔루션,기획,비매출,S/W 개발,솔루션,총괄기획실,협업증진팀,성형일,팀 기획업무회의 후 식사,"112,000",0,"112,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,20110101,LIA-101,외상매입금,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 ERP서버 인터넷 회선 통신비 26년 02월 결제의 건,0,"561,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,60116199,OP-204,통신비(기타),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 ERP서버 인터넷 회선 통신비 26년 02월 결제의 건,"510,000",0,"510,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,10115301,AST-106,매입세액,X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,SK브로드밴드(주),바론 ERP서버 인터넷 회선 통신비 26년 02월 결제의 건,"51,000",0,"51,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,20111109,LIA-101,미지급금(하나카드),Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,하나카드(6669),서산-아산 건설사업단 화상회의 S/W(Zoom) 1년 구독의 건,0,"246,070",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-13,2월,50153199,IT-301,원가)지급수수료(기타),Y25019,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구) 통합 스마트상황실 구축 및 운영,인주~염치(2공구)빅룸,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,,Zoom Video Communications Inc.,서산-아산 건설사업단 화상회의 S/W(Zoom) 1년 구독의 건,"246,070",0,"246,070",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-13,2월,10111101,REV-101,용역미수금(계산서발행),Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,2026경영및기술지원서비스(한맥기술)2월,"122,916,200",0,0,"122,916,200",,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-13,2월,40110401,REV-101,관리용역수입,Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,2026경영및기술지원서비스(한맥기술)2월,0,"111,742,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-13,2월,20112901,LIA-101,매출세액,Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,"111,742,000*10%",0,"11,174,200",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-13,2월,10111101,REV-101,용역미수금(계산서발행),Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,한맥기술,2026경영및기술지원서비스(한맥기술)2월,0,"122,916,200",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-13,2월,10110501,AST-101,보통예금,Y26008,2026 경영 및 기술지원 서비스(한맥기술),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(한맥기술)2월,"122,916,200",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-19,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 CHAT GPT 구독료(2월),0,"29,000",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-19,2월,60116399,IT-301,지급수수료(기타),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 CHAT GPT 구독료(2월),"29,000",0,"29,000",0,공통 → 경영진,구매,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-19,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,2월 업무관련 유류대(장종찬사장),0,"324,500",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-19,2월,60115999,WF-201,여비교통비(기타),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,2월 업무관련 유류대(장종찬사장),"324,500",0,"324,500",0,공통 → 경영진,출장비,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,솔루션통합팀,권혁진,건설기술인 승급 ( 중급 : 스마트건설 - BIM ) 교육 ( 설계시공 > 안전관리 > 건설안전 ),0,"200,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,60117301,WF-301,교육훈련비,ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,솔루션통합팀,권혁진,건설기술인 승급 ( 중급 : 스마트건설 - BIM ) 교육 ( 설계시공 > 안전관리 > 건설안전 ),"200,000",0,"200,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,20111109,LIA-101,미지급금(하나카드),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,하나카드(7726),1월 업무관련 식대(최대선),0,"380,000",0,0,공통 → OB,제외,공통,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,50152507,WF-101,원가)복리후생비(회식대),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,1월 업무관련 식대(최대선),"380,000",0,"380,000",0,공통 → OB,복리후생비,공통,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,20111109,LIA-101,미지급금(하나카드),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,하나카드(7726),1월 업무관련 유류대(최대선),0,"196,000",0,0,공통 → OB,제외,공통,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,60115999,WF-201,여비교통비(기타),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,1월 업무관련 유류대(최대선),"66,000",0,"66,000",0,공통 → OB,출장비,공통,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,60115999,WF-201,여비교통비(기타),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,1월 업무관련 유류대(최대선),"60,000",0,"60,000",0,공통 → OB,출장비,공통,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,60115999,WF-201,여비교통비(기타),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,최대선,1월 업무관련 유류대(최대선),"70,000",0,"70,000",0,공통 → OB,출장비,공통,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,시내교통비,0,"12,700",0,0,공통 → 대산당진BIM,제외,일반,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,50152705,WF-201,원가)여비교통비(시내교통비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,업무협의,"12,700",0,"12,700",0,공통 → 대산당진BIM,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,"업체방문 석식대 ( 바우컨설탄트, 총 6명 )",0,"189,100",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-19,2026-02-20,2월,60115705,WF-101,복리후생비(회식대),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,"업체방문 석식대 ( 바우컨설탄트, 총 6명 )","189,100",0,"189,100",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-02-20,2월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비,"93,200",0,"93,200",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-08,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비 - 김원식,0,"93,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-02-20,2월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비,"113,300",0,"113,300",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-16,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비 - 김원식,0,"113,300",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-20,2월,60115903,WF-201,여비교통비(국내출장비),Z25059,울산~외곽순환 고속도로(1공구),울산외곽순환(1공구),울산~외곽순환 고속도로(1공구),직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비,"106,400",0,"106,400",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-01-30,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),Z25059,울산~외곽순환 고속도로(1공구),울산외곽순환(1공구),울산~외곽순환 고속도로(1공구),직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비 - 김원식,0,"106,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-20,2월,60115903,WF-201,여비교통비(국내출장비),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비,"97,000",0,"97,000",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-12,2026-02-20,2월,20111103,LIA-101,미지급금(일반미지급),Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,총괄기획실,김원식,출장비 - 김원식,0,"97,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111103,LIA-101,미지급금(일반미지급),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,인프라 BIM1팀,김원기A,경부선 드론 기반 조사 촬영에 필요한 현장 운영비(전도금),0,"2,000,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,10114903,AST-105,전도금(가불금),Y26018,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역,경부선 (평택-직지사) 측량,경부선 평택-직지사구간 작업자 이동통로 확보 기술조사 및 실시설계 용역-드론촬영/현장조사자료작성,직접매출,기술용역계약,,매출,바론계약,기술용역,총괄기획실,인프라 BIM1팀,김원기A,경부선 드론 기반 조사 촬영에 필요한 현장 운영비(전도금),"2,000,000",0,"2,000,000",0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,60110101,HR-101,급여(임.직원),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,총괄/장종찬,2026년 2월 급여(바론),"393,784,100",0,"393,784,100",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111503,LIA-101,예수금(근로소득주민세),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,2025년 연말정산 (주민세),0,"-6,354,890",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111507,HR-201,예수금(국민연금),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,김예슬 국민연금 정산,0,"-688,020",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111501,LIA-101,예수금(근로소득세),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,2026년 2월 급여 근로소득세(바론),0,"26,421,200",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111503,LIA-101,예수금(근로소득주민세),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,근로복지공단,2026년 2월 급여 근로소득주민세(바론),0,"2,641,790",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111509,HR-203,예수금(고용보험료(직원)),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,고용노동부,2026년 2월 급여 고용보험(바론),0,"3,271,310",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111505,HR-202,예수금(건강보험료),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민건강보험공단,"2026년 2월 급여 의료보험(건강+요양, 바론)",0,"14,670,700",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111507,HR-201,예수금(국민연금),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민연금관리공단,2026년 2월 급여 국민연금(바론),0,"11,935,950",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111599,LIA-101,예수금(기타),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,2026년 2월 식대 및 기타 공제(바론),0,"1,796,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,총괄/장종찬,2026년 2월 급여(바론),0,"403,645,110",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-23,2월,20111501,LIA-101,예수금(근로소득세),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,,2025년 연말정산 (소득세),0,"-63,555,450",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-20,2026-02-24,2월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,조태희,인재성장팀 회식비(2026년 2월),0,"126,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-20,2026-02-24,2월,60115705,WF-101,복리후생비(회식대),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,조태희,인재성장팀 회식비(2026년 2월),"126,000",0,"126,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-24,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),2월 휴대폰 사용요금(최대선),0,"144,330",0,0,공통 → OB,제외,공통,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-24,2월,60116199,OP-204,통신비(기타),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),2월 휴대폰 사용요금(최대선),"37,633",0,"37,633",0,공통 → OB,구매,공통,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-24,2월,10115301,AST-106,매입세액,ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),2월 휴대폰 사용요금(최대선),"3,762",0,"3,762",0,공통 → OB,구매,공통,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-24,2월,60116199,OP-204,통신비(기타),ZZZZZZ,공통(OB),공통(OB),공통,기획/관리,공통,,공통,공통,공통,임원실,,SK텔레콤(주),2월 휴대폰 사용요금(최대선),"102,935",0,"102,935",0,공통 → OB,구매,공통,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-25,2월,10111101,REV-101,용역미수금(계산서발행),Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,2026경영및기술지원서비스(한라산업개발),0,"10,285,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-25,2월,10110501,AST-101,보통예금,Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(한라산업개발),"10,285,000",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-25,2월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민은행,퇴직연금(국민) 2026년 2월분 (44명),0,"19,360,430",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-25,2월,60111101,HR-103,퇴직금,X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,국민은행,퇴직연금(국민) 2026년 2월분 (44명),"19,360,430",0,"19,360,430",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-25,2월,20111103,LIA-101,미지급금(일반미지급),X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,신한은행,퇴직연금(신한) 2026년 2월분 (6명),0,"2,159,060",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-25,2월,60111101,HR-103,퇴직금,X26003,인사/교육,인사/교육,인사/교육,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,신한은행,퇴직연금(신한) 2026년 2월분 (6명),"2,159,060",0,"2,159,060",0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-25,2월,10111101,REV-101,용역미수금(계산서발행),Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,2026경영및기술지원서비스(삼안)2월,"294,607,500",0,0,"294,607,500",,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-25,2월,40110401,REV-101,관리용역수입,Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,2026경영및기술지원서비스(삼안)2월,0,"267,825,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-25,2월,20112901,LIA-101,매출세액,Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,"267,825,000*10%",0,"26,782,500",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-25,2월,10111101,REV-101,용역미수금(계산서발행),Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,2026경영및기술지원서비스(한라산업개발),"10,285,000",0,0,"10,285,000",,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-25,2월,40110401,REV-101,관리용역수입,Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,2026경영및기술지원서비스(한라산업개발),0,"9,350,000",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-10,2026-02-25,2월,20112901,LIA-101,매출세액,Y26012,2026 경영 및 기술지원 서비스(한라산업개발),,,,,,,,,총괄기획실,,한라산업개발,"9,350,000*10%",0,"935,000",0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-25,2월,10111101,REV-101,용역미수금(계산서발행),Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,삼안,2026경영및기술지원서비스(삼안)2월,0,"294,607,500",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-25,2월,10110501,AST-101,보통예금,Y26011,2026 경영 및 기술지원 서비스(삼안),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(삼안)2월,"294,607,500",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-26,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,0,"37,000",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-26,2월,60115705,WF-101,복리후생비(회식대),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,"37,000",0,"37,000",0,공통 → 경영진,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-26,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 AI 구독료(2월),0,"29,000",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-26,2월,60116399,IT-301,지급수수료(기타),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,임원 AI 구독료(2월),"29,000",0,"29,000",0,공통 → 경영진,구매,기타,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-26,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,0,"59,900",0,0,공통 → 경영진,제외,기타,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-26,2월,60115705,WF-101,복리후생비(회식대),ZZZZZZ,경영진,경영진,경영진,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,총괄기획실 사장,장종찬,업무 회의 후 식사,"59,900",0,"59,900",0,공통 → 경영진,복리후생비,기타,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,협업증진팀,한승민,"2월 한맥 농구동호회 대관비 (2회분, 2/11, 25)",0,"320,000",0,0,"공통 → 교육훈련,참석",제외,기타,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,60115798,WF-106,복리후생비(공통),ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,협업증진팀,한승민,"2월 한맥 농구동호회 대관비 (2회분, 2/11, 25)","320,000",0,"320,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-27,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"142,370",0,"142,370",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,출장비 - 권혁진,0,"8,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-11,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"134,370",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-27,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"69,920",0,"69,920",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-13,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"69,920",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,김우진,변리사 업무미팅 후 식사(IP 이전),0,"36,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60115705,WF-101,복리후생비(회식대),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,김우진,변리사 업무미팅 후 식사(IP 이전),"36,000",0,"36,000",0,,복리후생비,일반,,,,,,,,,
|
||||
바론,2026-02-20,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/13),0,"34,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-20,2026-02-27,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/13),"34,000",0,"34,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-20,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/13),0,"3,400",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-20,2026-02-27,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/13),"3,400",0,"3,400",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,바론 입찰제출용 법인인감증명서 발급수수료,0,"20,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-23,2026-02-27,2월,60116399,IT-301,지급수수료(기타),X26002,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,경영기획팀,국혜림,바론 입찰제출용 법인인감증명서 발급수수료,"20,000",0,"20,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,DB손해보험,바론컨설턴트 법인차량(168러 2890) 자동차보험 가입의 건,0,"743,590",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60115103,OP-201,보험료(자동차보험료),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,DB손해보험,바론컨설턴트 법인차량(168러 2890) 자동차보험 가입의 건,"743,590",0,"743,590",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-27,2월,60115903,WF-201,여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비,"111,950",0,"111,950",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,권혁진,출장비 - 권혁진,0,"30,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-24,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,출장비 - 염승호,0,"81,950",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/24),0,"34,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/24),"34,000",0,"34,000",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,디자인기획팀,신혜영,디자인팀 공용 피그마 사용료(2026.2월~2027.2월사용),0,"625,209",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60114399,OP-203,소모품비(기타(공통)),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,디자인기획팀,신혜영,디자인팀 공용 피그마 사용료(2026.2월~2027.2월사용),"625,209",0,"625,209",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,20111109,LIA-101,미지급금(하나카드),X25030,GSIM,GSIM,GSIM,S/W개발,솔루션,,비매출,S/W 개발,솔루션,기술개발센터,,하나카드(6669),GSIM팀 AWS 사용료(26년 1월),0,"182,932",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,50153199,IT-301,원가)지급수수료(기타),X25030,GSIM,GSIM,GSIM,S/W개발,솔루션,개발,비매출,S/W 개발,솔루션,기술개발센터,,Amazon Web Services Inc.,GSIM팀 AWS 사용료(26년 1월),"182,932",0,"182,932",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/26),0,"31,700",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,60115798,WF-106,복리후생비(공통),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,염승호,이지빔 영업(02/26),"31,700",0,"31,700",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,건국대학교 강의 지원,0,"16,940",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,50152701,WF-201,원가)여비교통비(국내출장비),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,건국대학교 강의 지원,"16,940",0,"16,940",0,,출장비,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 홍보 브로슈어 업체 발송,0,"165,440",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,60116399,IT-301,지급수수료(기타),X25026,EG-BIM Drawer,EG-BIM Drawer,EG-BIM Drawer,S/W개발,그래픽,영업,비매출,S/W 개발,그래픽&구조해석,총괄기획실,솔루션통합팀,김지영A,SW 홍보 브로슈어 업체 발송,"165,440",0,"165,440",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,10111101,REV-101,용역미수금(계산서발행),Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,장헌산업,2026경영및기술지원서비스(장헌산업),0,"18,241,300",0,0,,수입,기타,,,,,,,,,
|
||||
바론,2026-02-27,2026-02-27,2월,10110501,AST-101,보통예금,Y26009,2026 경영 및 기술지원 서비스(장헌산업),,,,,,,,,총괄기획실,,국민(주거래),2026경영및기술지원서비스(장헌산업),"18,241,300",0,0,0,,제외,기타,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,20111109,LIA-101,미지급금(하나카드),X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,하나카드(6669),Ai셀 Cloudflare 사용료(26년 1월),0,"8,319",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,50153199,IT-301,원가)지급수수료(기타),X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,Cloudflare Inc.,Ai셀 Cloudflare 사용료(26년 1월),"8,319",0,"8,319",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,20110101,LIA-101,외상매입금,X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,Ai셀 구글클라우드 Ai 사용료(26년 1월),0,"327,153",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,50153199,IT-301,원가)지급수수료(기타),X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,Ai셀 구글클라우드 Ai 사용료(26년 1월),"297,412",0,"297,412",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-26,2026-02-27,2월,10115301,AST-106,매입세액,X25054,AI,AI,AI,기획/관리,기획&관리,,비매출,기획/제안,기획,총괄기획실,,구글클라우드 코리아 유한회사,Ai셀 구글클라우드 Ai 사용료(26년 1월),"29,741",0,"29,741",0,,구매,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,20111103,LIA-101,미지급금(일반미지급),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,기술개발센터 공용 무선 랜카드 구매,0,"34,000",0,0,,제외,일반,,,,,,,,,
|
||||
바론,2026-02-25,2026-02-27,2월,60114301,OP-203,소모품비(사무용품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,인재성장팀,주완기,기술개발센터 공용 무선 랜카드 구매,"34,000",0,"34,000",0,,구매,일반,,,,,,,,,
|
||||
한맥,2026-02-10,2026-02-13,2월,50152599,WF-106,원가)복리후생비,ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,,방재보수교육 김승호 수석,"220,000",,"220,000",0,"공통 → 교육훈련,참석",구매,기타,,,,,,,,,
|
||||
한맥,2026-02-19,2026-02-19,2월,10113701,AST-104,선급금,ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,기술개발센터,,,대학원등록금 기업분담금(급여차감예정),"528,500",,0,"528,500",장헌 전상현JM (명지대 등록금건) → 제외,수입,공통,,,,,,,,,
|
||||
한맥,2026-02-26,2026-02-26,2월,60115305,MK-201,접대비,ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,기술개발센터,,,우종일 모친상/보훈병원장례식장,"100,000",,"100,000",0,"공통 → 교육훈련,참석",복리후생비,기타,,,,,,,,,
|
||||
한맥,2026-02-05,2026-02-13,2월,50151101,OP-101,원가)지급임차료,Z25056,고속국도 제 30호 서산-영덕선(대산-당진) 건설공사 전환 및 시공 BIM 수행 용역(제 2공구),대산~당진(2공구) 시공BIM,대산~당진 시공2공구 시공BIM 수행,직접매출,내부BIM설계지원,,매출,가족사 프로젝트,BIM 설계,총괄기획실,,,월세(2월) : 대산-당진(제2공구) 시공BIM 용역 현장 직원 숙소,"370,000",,"370,000",0,,구매,일반,,,,,,,,,
|
||||
한맥,2026-02-10,2026-02-13,2월,50153103,OUT-102,원가)지급수수료,ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/1분기 카페트 청소,"308,000",,"308,000",0,,구매,공통,,,,,,,,,
|
||||
한맥,2026-02-10,2026-02-13,2월,50153103,OUT-102,원가)지급수수료,ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,기술개발센터 ADT캡스 월정료_26년 2월,"99,000",,"99,000",0,,구매,공통,,,,,,,,,
|
||||
한맥,2026-02-11,2026-02-13,2월,50151109,OP-202,원가)지급임차료,ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/정수기 4대 26년 2월 청구분,"137,400",,"137,400",0,,구매,공통,,,,,,,,,
|
||||
한맥,2026-02-11,2026-02-13,2월,50152705,WF-201,원가)여비교통비,ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/2월 주차정기권,"99,000",,"99,000",0,,출장비,공통,,,,,,,,,
|
||||
한맥,2026-02-11,2026-02-13,2월,60116101,IT-401,통신비,ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/통신요금(26년 02월),"614,900",,"614,900",0,,구매,공통,,,,,,,,,
|
||||
한맥,2026-02-11,2026-02-13,2월,50152705,WF-201,원가)여비교통비,ZZZZZZ,공통,공통(기타),공통,기획/관리,공통,,공통,공통,공통,총괄기획실,,,마천사무실/2월 주차정기권,"33,000",,"33,000",0,,출장비,공통,,,,,,,,,
|
||||
한맥,2026-02-19,2026-02-23,2월,60114743,WF-103,경상시험연구비,ZZZZZZ,"교육훈련,참석","교육훈련, 참석","교육훈련,참석",기획/관리,공통,,공통,공통,공통,총괄기획실,,,기술개발센터 세미나 진행 관련 식사 및 다과 구입,"732,000",,"732,000",0,"공통 → 교육훈련,참석",복리후생비,기타,,,,,,,,,
|
||||
삼안,2026-02-10,2026-01-29,1월,60114739,IT-201,경상시험연구비(소모품비),X26005,운영지원(총무),운영지원(총무),운영지원(총무),기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,기술개발센터,,,센터 소모품 및 다과 구매,"4,128,674",,"4,128,674",0,,구매,일반,,,,,,,,,
|
||||
장헌산업,2025-12-16,2026-01-20,1월,823,OUT-201,연구개발비,20-교영-18,스마트건설기술개발사업 10과제(한맥/삼안/장헌/피티씨),스마트건설기술(10과제),스마트건설(10과제),직접매출,R&D,,매출,가족사 프로젝트,R&D,,,MDPI,스마트건설/MDPI 게재료(SCI),"4,870,413",,"4,870,413",0,,구매,R&D,,,,,,,,,
|
||||
장헌산업,2026-01-30,2026-02-04,2월,831,OUT-201,지급수수료,26-관리-06,경영기획/전략,경영기획/전략,경영기획/전략,기획/관리,총괄기획실,,총괄기획실,총괄기획실,총괄기획실,총괄기획실,,법무사 조수호,주식회사 장헌파트너스 분할 관련 공과금 추가납부(공증료),"121,000",,"121,000",0,,구매,일반,,,,,,,,,
|
||||
|
1622
incoming-files/payment.html
Normal file
1622
incoming-files/payment.html
Normal file
File diff suppressed because it is too large
Load Diff
90
incoming-files/ptj.csv
Normal file
90
incoming-files/ptj.csv
Normal file
@@ -0,0 +1,90 @@
|
||||
매출/비매출,분야,세부분야,프로젝트명,
|
||||
매출,바론계약,판매,EG-BIM (파이프텍코리아),
|
||||
매출,바론계약,판매,ERP(한국종합엔지니어링),
|
||||
매출,바론계약,판매,GAIA 기능 개선 계약,
|
||||
매출,바론계약,기술용역,계양-강화 고속도로 건설 기본 및 실시(5공구),
|
||||
매출,바론계약,기술용역,국도42호선 원주 흥업 사제 외 1개소 교차로 개선공사 실시설계용역(삼안),
|
||||
매출,바론계약,기술용역,인주-염치(제1공구) 회의 시스템 설치,
|
||||
매출,바론계약,기술용역,고속국도 제32호 당진~청주선 인주~염치간 건설공사(제2공구),
|
||||
매출,바론계약,기술용역,대산-당진 1공구 Conference Platform(BigRoom)구축,
|
||||
매출,바론계약,기술용역,대산-당진 2공구 Conference Platform(BigRoom)구축,
|
||||
매출,바론계약,기술용역,대산-당진 3-4공구 Conference Platform(BigRoom)구축,
|
||||
매출,바론계약,기술용역,공학용 사이니지 시스템 구축,2월 추가
|
||||
매출,바론계약,콘텐츠 제작,보은국토도로 사업관리,
|
||||
매출,바론계약,콘텐츠 제작,한강교량 사업관리(2공구),
|
||||
매출,바론계약,콘텐츠 제작,예산국토 제2권역,
|
||||
매출,바론계약,콘텐츠 제작,가평군 하수관망 계측시스템 유지관리 용역,
|
||||
매출,바론계약,콘텐츠 제작,PQ(지오메카이엔지),
|
||||
매출,가족사 프로젝트,BIM 설계,대산~당진 시공2공구 시공BIM 수행,
|
||||
매출,가족사 프로젝트,BIM 설계,서산~명천 도로건설공사 기본 및 실시설계,
|
||||
매출,가족사 프로젝트,BIM 설계,울산~외곽순환 고속도로(1공구),
|
||||
매출,가족사 프로젝트,BIM 설계,충남논산스마트팜 조감도,
|
||||
매출,가족사 프로젝트,BIM 설계,원효대교북단 조감도,
|
||||
매출,가족사 프로젝트,BIM 설계,사천시 향촌지구 우수유출저감시설 PQ 문서편집,
|
||||
매출,가족사 프로젝트,시공,인천발 KTX 직결교량 시공,
|
||||
매출,가족사 프로젝트,디자인,가족사 보고서 템플릿 제작_PQ,
|
||||
매출,가족사 프로젝트,디자인,(주)삼안 2026년 경영기획서 편집디자인,2월 추가
|
||||
매출,가족사 프로젝트,R&D,디지털 국토정보 기술개발사업,
|
||||
매출,가족사 프로젝트,R&D,국토정보 2핵심,
|
||||
매출,가족사 프로젝트,R&D,스마트건설(10과제),
|
||||
매출,가족사 프로젝트,R&D,XR기반 건설설계 혁신시스템,
|
||||
비매출,S/W 개발,기초조사 및 GIS,KNGIL,
|
||||
비매출,S/W 개발,기초조사 및 GIS,GIS Mapper,
|
||||
비매출,S/W 개발,기초조사 및 GIS,천지인,
|
||||
비매출,S/W 개발,기초조사 및 GIS,Surveyor,
|
||||
비매출,S/W 개발,기초조사 및 GIS,GAIA,
|
||||
비매출,S/W 개발,도로,WayPrimal,
|
||||
비매출,S/W 개발,도로,WayConfirm,
|
||||
비매출,S/W 개발,도로,WayDraw,
|
||||
비매출,S/W 개발,도로,WayShop,
|
||||
비매출,S/W 개발,구조물,WallZainer,
|
||||
비매출,S/W 개발,구조물,Bridge planner,
|
||||
비매출,S/W 개발,구조물,AbutZainer,
|
||||
비매출,S/W 개발,구조물,BriZainer-DR,
|
||||
비매출,S/W 개발,구조물,BriZainer-Nodular,
|
||||
비매출,S/W 개발,구조물,TunnelZainer,
|
||||
비매출,S/W 개발,구조,BoxZainer,2월 추가
|
||||
비매출,S/W 개발,교통,TOVA,
|
||||
비매출,S/W 개발,수리/수문,LifeLine-Water,
|
||||
비매출,S/W 개발,수리/수문,강우강도산정 프로그램,
|
||||
비매출,S/W 개발,그래픽&구조해석,HmEG(HmDraw),
|
||||
비매출,S/W 개발,그래픽&구조해석,EG-BIM Modeler,
|
||||
비매출,S/W 개발,그래픽&구조해석,EG-BIM Drawer,
|
||||
비매출,S/W 개발,그래픽&구조해석,StrAna,
|
||||
비매출,S/W 개발,솔루션,문서관리시스템(PM),
|
||||
비매출,S/W 개발,솔루션,bCMf,
|
||||
비매출,S/W 개발,솔루션,GSIM,
|
||||
비매출,S/W 개발,솔루션,CCP,
|
||||
비매출,S/W 개발,솔루션,단가/공정 solution,
|
||||
비매출,S/W 개발,솔루션,WatchBIM,
|
||||
비매출,S/W 개발,솔루션,Twin Highway,
|
||||
비매출,S/W 개발,솔루션,Domainer,
|
||||
비매출,S/W 개발,솔루션,Cadaster,2월 추가
|
||||
비매출,S/W 개발,운영S/W,입찰정보(용역/공사) 조회 시스템,
|
||||
비매출,S/W 개발,운영S/W,ERP: 장헌산업,
|
||||
비매출,S/W 개발,운영S/W,ERP: PTC,
|
||||
비매출,S/W 개발,운영S/W,ERP: 한라,
|
||||
비매출,S/W 개발,운영S/W,ERP: 삼안,
|
||||
비매출,S/W 개발,운영S/W,ERP: 한맥,
|
||||
비매출,S/W 개발,운영S/W,ERP: 바론,
|
||||
비매출,S/W 개발,운영S/W,ERP: (주)장헌,
|
||||
비매출,S/W 개발,운영S/W,ERP: 산하종합기술,
|
||||
비매출,S/W 개발,운영S/W,CivilEngineeringLab,
|
||||
비매출,S/W 개발,운영S/W,PQ시스템,
|
||||
비매출,S/W 개발,운영S/W,BEPs,
|
||||
비매출,S/W 개발,운영S/W,전산운영관리,
|
||||
비매출,S/W 개발,운영S/W,한맥가족 배움터,
|
||||
비매출,기획/제안,기획,사전기획,
|
||||
비매출,기획/제안,기획,청용천교 업무지원,
|
||||
비매출,기획/제안,기획,수자원 해외사업,
|
||||
비매출,기획/제안,기획,AI,
|
||||
비매출,기획/제안,기획,GIS장비 개발,
|
||||
비매출,기획/제안,기획,프리캐스트 조립식 박스 교량,
|
||||
비매출,기획/제안,기획,인덕원~동탄 복선전철 스마트상황실 구축 및 운영안,
|
||||
비매출,기획/제안,기획,바론 SW 포탈,2월 추가
|
||||
총괄기획실,총괄기획실,총괄기획실,경영기획/전략,
|
||||
총괄기획실,총괄기획실,총괄기획실,인사/교육,
|
||||
총괄기획실,총괄기획실,총괄기획실,운영지원(총무),
|
||||
총괄기획실,총괄기획실,총괄기획실,업무/사업관리,
|
||||
공통,공통,공통,"교육훈련,참석",
|
||||
공통,공통,공통,공통,
|
||||
|
13
incoming-files/reference/README.md
Normal file
13
incoming-files/reference/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Reference Assets
|
||||
|
||||
이 디렉터리는 앞으로 `8081`에서 직접 서빙하지 않는 참고 원본/복구 비교 자산을 모으기 위한 공간이다.
|
||||
|
||||
1차 정리에서는 위험한 대량 이동을 피하기 위해 기존 참고 파일을 즉시 옮기지 않는다.
|
||||
대신 실제 서빙 파일은 `incoming-files/served/`로 고정하고, 다음 차수에서 참고 자산을 단계적으로 재배치한다.
|
||||
|
||||
예상 대상:
|
||||
|
||||
- 원본 HTML/CSS 참고본
|
||||
- 원본 xlsx/csv
|
||||
- 복구 비교용 자산
|
||||
- 디자인 레퍼런스 파일
|
||||
1377
incoming-files/sample style.css
Normal file
1377
incoming-files/sample style.css
Normal file
File diff suppressed because it is too large
Load Diff
931
incoming-files/seat/center_chair_people_map(2).html
Normal file
931
incoming-files/seat/center_chair_people_map(2).html
Normal file
@@ -0,0 +1,931 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>center chair people map</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #152330;
|
||||
--muted: #627286;
|
||||
--paper: rgba(255,255,255,0.86);
|
||||
--line: rgba(21,35,48,0.1);
|
||||
--accent: #0f766e;
|
||||
--bg: #edf2f6;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "IBM Plex Sans KR", "Pretendard", sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(15,118,110,0.11), transparent 22%),
|
||||
linear-gradient(180deg, #f5f8fb 0%, #e8eef3 100%);
|
||||
}
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 10px 14px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #0f766e, #115e59);
|
||||
box-shadow: 0 10px 22px rgba(15,118,110,0.18);
|
||||
}
|
||||
button.alt {
|
||||
color: var(--ink);
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: none;
|
||||
}
|
||||
.viewer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.viewer-head {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chip {
|
||||
padding: 10px 12px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.82);
|
||||
border: 1px solid rgba(255,255,255,0.94);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 24px rgba(21,35,48,0.08);
|
||||
}
|
||||
.viewer-actions {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 64px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.mapper {
|
||||
position: absolute;
|
||||
top: 76px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(94vw, 1320px);
|
||||
max-height: min(56vh, 560px);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
border-radius: 20px;
|
||||
background: rgba(234, 239, 247, 0.95);
|
||||
border: 1px solid rgba(101, 119, 146, 0.22);
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.hidden-off {
|
||||
display: none !important;
|
||||
}
|
||||
.mapper-head {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid rgba(101,119,146,0.18);
|
||||
font-size: 12px;
|
||||
color: #51607a;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
.mapper-head strong {
|
||||
display: block;
|
||||
color: #17243b;
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.mapper-head .alt {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.org-chart {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.org-top {
|
||||
margin: 0 auto;
|
||||
width: min(100%, 420px);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(67, 84, 118, 0.25);
|
||||
background: #fff;
|
||||
}
|
||||
.org-top-title {
|
||||
background: #1e2f4d;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
padding: 16px 12px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.org-top-members {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
}
|
||||
.org-teams {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.org-team {
|
||||
border: 1px solid rgba(110, 126, 152, 0.25);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(255,255,255,0.95);
|
||||
min-width: 0;
|
||||
}
|
||||
.org-team h4 {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
font-size: 14px;
|
||||
color: #21324e;
|
||||
font-weight: 800;
|
||||
border-bottom: 1px solid rgba(110, 126, 152, 0.2);
|
||||
background: rgba(240, 245, 252, 0.96);
|
||||
}
|
||||
.org-members {
|
||||
padding: 7px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.org-person {
|
||||
border: 1px solid rgba(116, 133, 161, 0.25);
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease;
|
||||
min-width: 0;
|
||||
}
|
||||
.org-person.active {
|
||||
border-color: rgba(15,118,110,0.6);
|
||||
background: rgba(15,118,110,0.11);
|
||||
}
|
||||
.org-person.assigned {
|
||||
border-color: rgba(37,99,235,0.5);
|
||||
background: rgba(37,99,235,0.1);
|
||||
}
|
||||
.org-person strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
color: #15233a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.org-person small {
|
||||
display: block;
|
||||
color: #5a6a86;
|
||||
font-size: 11px;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.mapper {
|
||||
top: 72px;
|
||||
width: min(96vw, 920px);
|
||||
max-height: 58vh;
|
||||
}
|
||||
.viewer-actions {
|
||||
top: 64px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mapper-head strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
.org-top-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
.org-teams {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
canvas.dragging { cursor: grabbing; }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
min-width: 170px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(17,24,39,0.94);
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translate(12px, 12px);
|
||||
transition: opacity 120ms ease;
|
||||
z-index: 3;
|
||||
}
|
||||
.tooltip.visible { opacity: 1; }
|
||||
.tooltip strong { display: block; margin-bottom: 6px; font-size: 14px; }
|
||||
.tooltip div { font-size: 12px; line-height: 1.45; color: rgba(255,255,255,0.82); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="shell">
|
||||
<main class="panel viewer">
|
||||
<div class="viewer-head">
|
||||
<div class="chip" id="scale-chip"></div>
|
||||
<div class="chip" id="hover-chip">chair hover: none</div>
|
||||
</div>
|
||||
<div class="viewer-actions">
|
||||
<button type="button" id="fit-btn">전체 맞춤</button>
|
||||
<button type="button" class="alt" id="clear-btn">선택 지우기</button>
|
||||
</div>
|
||||
<aside class="mapper hidden-off">
|
||||
<div class="mapper-head">
|
||||
<div id="mapper-status">
|
||||
<strong>조직 현황</strong>
|
||||
<span>선택 인원 없음</span>
|
||||
</div>
|
||||
<button type="button" class="alt" id="clear-assign-btn">매칭 초기화</button>
|
||||
</div>
|
||||
<div class="org-chart" id="org-chart"></div>
|
||||
</aside>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./center_chair_people_payload.js?v=20260330a"></script>
|
||||
<script>
|
||||
const DATA = window.CHAIR_MAP_DATA;
|
||||
function decodeSegments(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
||||
return new Int32Array(bytes.buffer);
|
||||
}
|
||||
const bgTileRanges = DATA.bgTileRanges;
|
||||
const bgSegValues = decodeSegments(DATA.bgSegsB64);
|
||||
const chairSegValues = decodeSegments(DATA.chairSegsB64);
|
||||
const chairs = DATA.chairs.map(([key, name, kind, start, count]) => ({
|
||||
key, name, kind, start, count
|
||||
}));
|
||||
const meta = DATA.meta;
|
||||
const world = meta.headerBounds;
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const tooltip = document.getElementById("tooltip");
|
||||
const scaleChip = document.getElementById("scale-chip");
|
||||
const hoverChip = document.getElementById("hover-chip");
|
||||
const STORAGE_KEY = "ptc-chair-selection";
|
||||
const PEOPLE_STORAGE_KEY = "ptc-chair-people";
|
||||
const ASSIGN_STORAGE_KEY = "ptc-chair-assignments";
|
||||
const ACTIVE_PERSON_STORAGE_KEY = "ptc-chair-active-person";
|
||||
const clearAssignBtn = document.getElementById("clear-assign-btn");
|
||||
const orgChartEl = document.getElementById("org-chart");
|
||||
const mapperStatus = document.getElementById("mapper-status");
|
||||
// Prevent stale auto-highlights from previous sessions.
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
localStorage.removeItem(ASSIGN_STORAGE_KEY);
|
||||
const placed = new Set();
|
||||
let people = JSON.parse(localStorage.getItem(PEOPLE_STORAGE_KEY) || "[]");
|
||||
let chairAssignments = {};
|
||||
let activePersonId = null;
|
||||
const ORG_TEMPLATE = {
|
||||
top: {
|
||||
name: "총괄기획실",
|
||||
count: 53,
|
||||
members: [
|
||||
{ name: "장종찬", dept: "총괄기획실", title: "기획실장" },
|
||||
{ name: "김원식", dept: "총괄기획실", title: "전무이사" },
|
||||
],
|
||||
},
|
||||
teams: [
|
||||
{ name: "경영기획팀", count: 6, members: ["김우진", "임민정", "국혜린", "최선아", "김윤재", "이미영"] },
|
||||
{ name: "인재성장팀", count: 5, members: ["조태희", "최근혜", "류원준", "주안기", "정성호"] },
|
||||
{ name: "ERP 기획팀", count: 5, members: ["류호성", "문형식", "최요제", "황대일", "이채봉"] },
|
||||
{ name: "디자인기획팀", count: 17, members: ["신혜영", "정은혜", "김태식", "최예은", "채선영", "최영환", "윤봄이", "이예진", "허유나", "마희연", "김수현", "박지영", "권순호", "정두휘", "김정석", "정지윤", "양숙영"] },
|
||||
{ name: "기술기획팀", count: 11, members: ["김원기", "홍아름", "이경민", "김혜인", "황동환", "최찬호", "이태훈", "김신지", "조찬영", "김용연", "한치영"] },
|
||||
{ name: "협업증진팀", count: 3, members: ["성형일", "박주한", "한승민"] },
|
||||
{ name: "솔루션통합팀", count: 4, members: ["권혁진", "염승호", "윤준수", "김지영"] },
|
||||
],
|
||||
};
|
||||
const chairGeometry = chairs.map((chair) => {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
const path = new Path2D();
|
||||
const hitSegments = new Float32Array(chair.count * 4);
|
||||
let segCursor = 0;
|
||||
for (let i = chair.start; i < chair.start + chair.count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = chairSegValues[offset] / 10;
|
||||
const y1 = chairSegValues[offset + 1] / 10;
|
||||
const x2 = chairSegValues[offset + 2] / 10;
|
||||
const y2 = chairSegValues[offset + 3] / 10;
|
||||
path.moveTo(x1, y1);
|
||||
path.lineTo(x2, y2);
|
||||
hitSegments[segCursor] = x1;
|
||||
hitSegments[segCursor + 1] = y1;
|
||||
hitSegments[segCursor + 2] = x2;
|
||||
hitSegments[segCursor + 3] = y2;
|
||||
segCursor += 4;
|
||||
minX = Math.min(minX, x1, x2);
|
||||
minY = Math.min(minY, y1, y2);
|
||||
maxX = Math.max(maxX, x1, x2);
|
||||
maxY = Math.max(maxY, y1, y2);
|
||||
}
|
||||
return {
|
||||
...chair,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
area: Math.max(1, (maxX - minX) * (maxY - minY)),
|
||||
path,
|
||||
hitSegments,
|
||||
};
|
||||
});
|
||||
function renumberChairKeys(chairItems) {
|
||||
if (!chairItems.length) return;
|
||||
const heights = chairItems
|
||||
.map((chair) => Math.max(1, chair.maxY - chair.minY))
|
||||
.sort((a, b) => a - b);
|
||||
const medianHeight = heights[Math.floor(heights.length / 2)] || 1;
|
||||
const rowTolerance = Math.max(40, medianHeight * 0.9);
|
||||
|
||||
const sorted = [...chairItems].sort((a, b) => {
|
||||
const ay = (a.minY + a.maxY) * 0.5;
|
||||
const by = (b.minY + b.maxY) * 0.5;
|
||||
if (Math.abs(by - ay) > rowTolerance) return by - ay; // top -> bottom
|
||||
const ax = (a.minX + a.maxX) * 0.5;
|
||||
const bx = (b.minX + b.maxX) * 0.5;
|
||||
return ax - bx; // left -> right
|
||||
});
|
||||
|
||||
sorted.forEach((chair, index) => {
|
||||
chair.key = String(index + 1);
|
||||
chair.seatNo = index + 1;
|
||||
});
|
||||
}
|
||||
renumberChairKeys(chairGeometry);
|
||||
const PICK_GRID_SIZE = 1800;
|
||||
const chairPickGrid = new Map();
|
||||
function pickGridKey(gx, gy) {
|
||||
return `${gx},${gy}`;
|
||||
}
|
||||
chairGeometry.forEach((chair, index) => {
|
||||
const minGX = Math.floor(chair.minX / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor(chair.maxX / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor(chair.minY / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor(chair.maxY / PICK_GRID_SIZE);
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const key = pickGridKey(gx, gy);
|
||||
if (!chairPickGrid.has(key)) chairPickGrid.set(key, []);
|
||||
chairPickGrid.get(key).push(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const camera = { scale: 1, offsetX: 0, offsetY: 0 };
|
||||
let pixelRatio = window.devicePixelRatio || 1;
|
||||
let pointer = { x: 0, y: 0 };
|
||||
let dragging = false;
|
||||
let dragStart = null;
|
||||
let hovered = null;
|
||||
let rafPending = false;
|
||||
|
||||
function normalizePeople(raw) {
|
||||
return raw
|
||||
.map((person, index) => {
|
||||
if (!person || !person.name) return null;
|
||||
return {
|
||||
id: person.id || `person-${index + 1}`,
|
||||
name: String(person.name).trim(),
|
||||
dept: String(person.dept || "").trim(),
|
||||
title: String(person.title || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function createTemplatePeople() {
|
||||
const generated = [];
|
||||
let seq = 1;
|
||||
ORG_TEMPLATE.top.members.forEach((member) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name: member.name,
|
||||
dept: member.dept,
|
||||
title: member.title,
|
||||
});
|
||||
});
|
||||
ORG_TEMPLATE.teams.forEach((team) => {
|
||||
team.members.forEach((name) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name,
|
||||
dept: team.name,
|
||||
title: "선임",
|
||||
});
|
||||
});
|
||||
});
|
||||
return generated;
|
||||
}
|
||||
|
||||
people = normalizePeople(people);
|
||||
const templateReady = people.some((person) => person.name === "장종찬" && person.dept === "총괄기획실");
|
||||
if (!templateReady) {
|
||||
people = createTemplatePeople();
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
const chairKeySet = new Set(chairGeometry.map((chair) => chair.key));
|
||||
chairAssignments = Object.fromEntries(
|
||||
Object.entries(chairAssignments).filter(([chairKey, personId]) => (
|
||||
chairKeySet.has(chairKey) && people.some((person) => person.id === personId)
|
||||
))
|
||||
);
|
||||
if (activePersonId && !people.some((person) => person.id === activePersonId)) activePersonId = null;
|
||||
|
||||
function persistPeople() {
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
|
||||
function persistAssignments() {
|
||||
localStorage.setItem(ASSIGN_STORAGE_KEY, JSON.stringify(chairAssignments));
|
||||
}
|
||||
|
||||
function persistActivePerson() {
|
||||
if (!activePersonId) localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
else localStorage.setItem(ACTIVE_PERSON_STORAGE_KEY, activePersonId);
|
||||
}
|
||||
|
||||
function assignmentCount() {
|
||||
return Object.keys(chairAssignments).length;
|
||||
}
|
||||
|
||||
function getPersonById(id) {
|
||||
return people.find((person) => person.id === id) || null;
|
||||
}
|
||||
|
||||
function getChairByPerson(personId) {
|
||||
for (const [chairKey, assignedPersonId] of Object.entries(chairAssignments)) {
|
||||
if (assignedPersonId === personId) return chairKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderPeopleList() {
|
||||
const activePerson = getPersonById(activePersonId);
|
||||
const countText = `${assignmentCount()} / ${people.length} 매칭`;
|
||||
mapperStatus.innerHTML = `<strong>조직 현황</strong><span>${activePerson ? `${activePerson.name} 선택됨` : "선택 인원 없음"} · ${countText}</span>`;
|
||||
|
||||
const findPerson = (dept, name) => people.find((person) => person.dept === dept && person.name === name) || null;
|
||||
const personCard = (person, roleText) => {
|
||||
if (!person) return "";
|
||||
const chairKey = getChairByPerson(person.id);
|
||||
const assignedClass = chairKey ? " assigned" : "";
|
||||
const activeClass = person.id === activePersonId ? " active" : "";
|
||||
return `
|
||||
<article class="org-person${assignedClass}${activeClass}" data-person-id="${person.id}">
|
||||
<strong>${person.name}</strong>
|
||||
<small>${person.title || roleText || "-"}</small>
|
||||
<small>${chairKey ? `좌석 ${chairKey}` : "좌석 미지정"}</small>
|
||||
</article>
|
||||
`;
|
||||
};
|
||||
|
||||
const topHtml = ORG_TEMPLATE.top.members
|
||||
.map((member) => personCard(findPerson(member.dept, member.name), member.title))
|
||||
.join("");
|
||||
|
||||
const teamsHtml = ORG_TEMPLATE.teams.map((team) => {
|
||||
const membersHtml = team.members
|
||||
.map((name) => personCard(findPerson(team.name, name), "선임"))
|
||||
.join("");
|
||||
return `
|
||||
<section class="org-team">
|
||||
<h4>${team.name} (${team.count})</h4>
|
||||
<div class="org-members">${membersHtml}</div>
|
||||
</section>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
orgChartEl.innerHTML = `
|
||||
<section class="org-top">
|
||||
<div class="org-top-title">${ORG_TEMPLATE.top.name} (${ORG_TEMPLATE.top.count})</div>
|
||||
<div class="org-top-members">${topHtml}</div>
|
||||
</section>
|
||||
<section class="org-teams">${teamsHtml}</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function worldToScreen(x, y) {
|
||||
return {
|
||||
x: x * camera.scale + camera.offsetX,
|
||||
y: (world.maxY - y + world.minY) * camera.scale + camera.offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
function screenToWorld(x, y) {
|
||||
return {
|
||||
x: (x - camera.offsetX) / camera.scale,
|
||||
y: world.maxY + world.minY - (y - camera.offsetY) / camera.scale,
|
||||
};
|
||||
}
|
||||
|
||||
function resize() {
|
||||
pixelRatio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.round(rect.width * pixelRatio);
|
||||
canvas.height = Math.round(rect.height * pixelRatio);
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
fit();
|
||||
}
|
||||
|
||||
function fit() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const width = world.maxX - world.minX;
|
||||
const height = world.maxY - world.minY;
|
||||
const pad = 36;
|
||||
const scaleX = (rect.width - pad * 2) / width;
|
||||
const scaleY = (rect.height - pad * 2) / height;
|
||||
camera.scale = Math.min(scaleX, scaleY);
|
||||
camera.offsetX = pad - world.minX * camera.scale + (rect.width - pad * 2 - width * camera.scale) / 2;
|
||||
camera.offsetY = pad - world.minY * camera.scale + (rect.height - pad * 2 - height * camera.scale) / 2;
|
||||
requestDraw();
|
||||
}
|
||||
|
||||
function drawGrid(width, height) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(21,35,48,0.05)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 120; x < width; x += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 120; y < height; y += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function pickChair(screenX, screenY) {
|
||||
const threshold = 12;
|
||||
const pointerWorld = screenToWorld(screenX, screenY);
|
||||
const thresholdWorld = threshold / camera.scale;
|
||||
const thresholdWorldSq = thresholdWorld * thresholdWorld;
|
||||
const minGX = Math.floor((pointerWorld.x - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor((pointerWorld.x + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor((pointerWorld.y - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor((pointerWorld.y + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const candidateIndexes = [];
|
||||
const seen = new Set();
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const candidates = chairPickGrid.get(pickGridKey(gx, gy));
|
||||
if (!candidates) continue;
|
||||
for (const index of candidates) {
|
||||
if (seen.has(index)) continue;
|
||||
seen.add(index);
|
||||
candidateIndexes.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
let best = null;
|
||||
for (const index of candidateIndexes) {
|
||||
const chair = chairGeometry[index];
|
||||
if (
|
||||
pointerWorld.x < chair.minX - thresholdWorld ||
|
||||
pointerWorld.x > chair.maxX + thresholdWorld ||
|
||||
pointerWorld.y < chair.minY - thresholdWorld ||
|
||||
pointerWorld.y > chair.maxY + thresholdWorld
|
||||
) continue;
|
||||
let distSq = Infinity;
|
||||
for (let i = 0; i < chair.hitSegments.length; i += 4) {
|
||||
const x1 = chair.hitSegments[i];
|
||||
const y1 = chair.hitSegments[i + 1];
|
||||
const x2 = chair.hitSegments[i + 2];
|
||||
const y2 = chair.hitSegments[i + 3];
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
let segDistSq;
|
||||
if (len2 === 0) {
|
||||
const px = pointerWorld.x - x1;
|
||||
const py = pointerWorld.y - y1;
|
||||
segDistSq = px * px + py * py;
|
||||
} else {
|
||||
let t = ((pointerWorld.x - x1) * dx + (pointerWorld.y - y1) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const lx = x1 + t * dx;
|
||||
const ly = y1 + t * dy;
|
||||
const px = pointerWorld.x - lx;
|
||||
const py = pointerWorld.y - ly;
|
||||
segDistSq = px * px + py * py;
|
||||
}
|
||||
if (segDistSq < distSq) distSq = segDistSq;
|
||||
if (distSq <= thresholdWorldSq * 0.3) break;
|
||||
}
|
||||
if (distSq > thresholdWorldSq) continue;
|
||||
const dist = Math.sqrt(distSq) * camera.scale;
|
||||
|
||||
if (!best) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
const distGap = dist - best.dist;
|
||||
if (distGap < -0.75) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Math.abs(distGap) <= 2) {
|
||||
const areaGap = chair.area - best.chair.area;
|
||||
if (areaGap < -1) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(areaGap) <= 1 &&
|
||||
chair.kind === "block" &&
|
||||
best.chair.kind !== "block"
|
||||
) {
|
||||
best = { chair, dist };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best ? best.chair : null;
|
||||
}
|
||||
|
||||
function renderTooltip() {
|
||||
if (!hovered) {
|
||||
tooltip.classList.remove("visible");
|
||||
hoverChip.textContent = "chair hover: none";
|
||||
return;
|
||||
}
|
||||
hoverChip.textContent = `chair hover: ${hovered.name}`;
|
||||
tooltip.innerHTML = `
|
||||
<strong>${hovered.name}</strong>
|
||||
<div>chair key: ${hovered.key}</div>
|
||||
<div>${placed.has(hovered.key) ? "선택됨" : "클릭하면 선택"}</div>
|
||||
<div>${chairAssignments[hovered.key] ? `배치: ${(getPersonById(chairAssignments[hovered.key]) || { name: "알수없음" }).name}` : "배치 인원 없음"}</div>
|
||||
`;
|
||||
tooltip.style.left = `${pointer.x + 14}px`;
|
||||
tooltip.style.top = `${pointer.y + 14}px`;
|
||||
tooltip.classList.add("visible");
|
||||
}
|
||||
|
||||
function requestDraw() {
|
||||
if (rafPending) return;
|
||||
rafPending = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
rafPending = false;
|
||||
draw();
|
||||
});
|
||||
}
|
||||
|
||||
function applyWorldTransform() {
|
||||
ctx.setTransform(
|
||||
pixelRatio * camera.scale,
|
||||
0,
|
||||
0,
|
||||
-pixelRatio * camera.scale,
|
||||
pixelRatio * camera.offsetX,
|
||||
pixelRatio * ((world.maxY + world.minY) * camera.scale + camera.offsetY)
|
||||
);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
drawGrid(rect.width, rect.height);
|
||||
const viewA = screenToWorld(0, rect.height);
|
||||
const viewB = screenToWorld(rect.width, 0);
|
||||
const viewMinX = Math.min(viewA.x, viewB.x);
|
||||
const viewMaxX = Math.max(viewA.x, viewB.x);
|
||||
const viewMinY = Math.min(viewA.y, viewB.y);
|
||||
const viewMaxY = Math.max(viewA.y, viewB.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.strokeStyle = "rgba(100, 116, 139, 0.28)";
|
||||
ctx.lineWidth = 1 / camera.scale;
|
||||
const tileSize = meta.backgroundTileSize;
|
||||
const tileMinX = Math.floor(viewMinX / tileSize);
|
||||
const tileMaxX = Math.floor(viewMaxX / tileSize);
|
||||
const tileMinY = Math.floor(viewMinY / tileSize);
|
||||
const tileMaxY = Math.floor(viewMaxY / tileSize);
|
||||
for (let tx = tileMinX; tx <= tileMaxX; tx += 1) {
|
||||
for (let ty = tileMinY; ty <= tileMaxY; ty += 1) {
|
||||
const range = bgTileRanges[`${tx},${ty}`];
|
||||
if (!range) continue;
|
||||
const start = range[0];
|
||||
const count = range[1];
|
||||
for (let i = start; i < start + count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = bgSegValues[offset] / 10;
|
||||
const y1 = bgSegValues[offset + 1] / 10;
|
||||
const x2 = bgSegValues[offset + 2] / 10;
|
||||
const y2 = bgSegValues[offset + 3] / 10;
|
||||
if (
|
||||
Math.max(x1, x2) < viewMinX ||
|
||||
Math.min(x1, x2) > viewMaxX ||
|
||||
Math.max(y1, y2) < viewMinY ||
|
||||
Math.min(y1, y2) > viewMaxY
|
||||
) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
hovered = dragging ? null : pickChair(pointer.x, pointer.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.lineWidth = 1.45 / camera.scale;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
for (const chair of chairGeometry) {
|
||||
if (chair.maxX < viewMinX || chair.minX > viewMaxX || chair.maxY < viewMinY || chair.minY > viewMaxY) continue;
|
||||
const active = hovered && hovered.key === chair.key;
|
||||
const selected = placed.has(chair.key);
|
||||
const assignedPersonId = chairAssignments[chair.key];
|
||||
const activePersonChair = activePersonId && assignedPersonId === activePersonId;
|
||||
const assigned = Boolean(assignedPersonId);
|
||||
const baseWidth = chair.kind === "block" ? 1.45 : 1.35;
|
||||
ctx.strokeStyle = activePersonChair
|
||||
? "rgba(234, 179, 8, 1)"
|
||||
: assigned
|
||||
? "rgba(37, 99, 235, 0.98)"
|
||||
: selected
|
||||
? "rgba(220, 38, 38, 0.98)"
|
||||
: active
|
||||
? "rgba(15, 118, 110, 0.98)"
|
||||
: chair.kind === "group"
|
||||
? "rgba(16, 134, 149, 0.74)"
|
||||
: "rgba(21, 149, 142, 0.8)";
|
||||
ctx.lineWidth = (activePersonChair ? 2.8 : assigned ? 2.4 : selected ? 2.6 : active ? 2.1 : baseWidth) / camera.scale;
|
||||
ctx.stroke(chair.path);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
scaleChip.textContent = `scale ${camera.scale.toFixed(4)}x`;
|
||||
renderTooltip();
|
||||
}
|
||||
|
||||
function persistPlaced() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify([...placed]));
|
||||
}
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
dragging = true;
|
||||
dragStart = { x: event.clientX, y: event.clientY, offsetX: camera.offsetX, offsetY: camera.offsetY };
|
||||
canvas.classList.add("dragging");
|
||||
});
|
||||
|
||||
window.addEventListener("pointerup", (event) => {
|
||||
if (dragging && dragStart) {
|
||||
const move = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
|
||||
if (move < 4) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const picked = pickChair(event.clientX - rect.left, event.clientY - rect.top);
|
||||
if (picked) {
|
||||
if (placed.has(picked.key)) placed.delete(picked.key);
|
||||
else placed.add(picked.key);
|
||||
persistPlaced();
|
||||
if (activePersonId) {
|
||||
const currentChair = getChairByPerson(activePersonId);
|
||||
if (chairAssignments[picked.key] === activePersonId) {
|
||||
delete chairAssignments[picked.key];
|
||||
} else {
|
||||
if (currentChair && currentChair !== picked.key) delete chairAssignments[currentChair];
|
||||
chairAssignments[picked.key] = activePersonId;
|
||||
}
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dragging = false;
|
||||
dragStart = null;
|
||||
canvas.classList.remove("dragging");
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
window.addEventListener("pointermove", (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
||||
if (dragging && dragStart) {
|
||||
camera.offsetX = dragStart.offsetX + (event.clientX - dragStart.x);
|
||||
camera.offsetY = dragStart.offsetY + (event.clientY - dragStart.y);
|
||||
}
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = event.clientX - rect.left;
|
||||
const my = event.clientY - rect.top;
|
||||
const before = screenToWorld(mx, my);
|
||||
const factor = event.deltaY < 0 ? 1.08 : 0.92;
|
||||
camera.scale = Math.max(0.002, Math.min(2, camera.scale * factor));
|
||||
const after = worldToScreen(before.x, before.y);
|
||||
camera.offsetX += mx - after.x;
|
||||
camera.offsetY += my - after.y;
|
||||
requestDraw();
|
||||
}, { passive: false });
|
||||
|
||||
document.getElementById("fit-btn").addEventListener("click", fit);
|
||||
document.getElementById("clear-btn").addEventListener("click", () => {
|
||||
placed.clear();
|
||||
persistPlaced();
|
||||
requestDraw();
|
||||
});
|
||||
clearAssignBtn.addEventListener("click", () => {
|
||||
chairAssignments = {};
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
orgChartEl.addEventListener("click", (event) => {
|
||||
const item = event.target.closest(".org-person[data-person-id]");
|
||||
if (!item) return;
|
||||
const personId = item.getAttribute("data-person-id");
|
||||
activePersonId = personId === activePersonId ? null : personId;
|
||||
persistActivePerson();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
window.addEventListener("resize", resize);
|
||||
renderPeopleList();
|
||||
resize();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
931
incoming-files/seat/center_chair_people_map.html
Normal file
931
incoming-files/seat/center_chair_people_map.html
Normal file
@@ -0,0 +1,931 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>center chair people map</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #152330;
|
||||
--muted: #627286;
|
||||
--paper: rgba(255,255,255,0.86);
|
||||
--line: rgba(21,35,48,0.1);
|
||||
--accent: #0f766e;
|
||||
--bg: #edf2f6;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "IBM Plex Sans KR", "Pretendard", sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(15,118,110,0.11), transparent 22%),
|
||||
linear-gradient(180deg, #f5f8fb 0%, #e8eef3 100%);
|
||||
}
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 10px 14px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #0f766e, #115e59);
|
||||
box-shadow: 0 10px 22px rgba(15,118,110,0.18);
|
||||
}
|
||||
button.alt {
|
||||
color: var(--ink);
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: none;
|
||||
}
|
||||
.viewer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.viewer-head {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chip {
|
||||
padding: 10px 12px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.82);
|
||||
border: 1px solid rgba(255,255,255,0.94);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 24px rgba(21,35,48,0.08);
|
||||
}
|
||||
.viewer-actions {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 64px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.mapper {
|
||||
position: absolute;
|
||||
top: 76px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(94vw, 1320px);
|
||||
max-height: min(56vh, 560px);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
border-radius: 20px;
|
||||
background: rgba(234, 239, 247, 0.95);
|
||||
border: 1px solid rgba(101, 119, 146, 0.22);
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.hidden-off {
|
||||
display: none !important;
|
||||
}
|
||||
.mapper-head {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid rgba(101,119,146,0.18);
|
||||
font-size: 12px;
|
||||
color: #51607a;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
.mapper-head strong {
|
||||
display: block;
|
||||
color: #17243b;
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.mapper-head .alt {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.org-chart {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.org-top {
|
||||
margin: 0 auto;
|
||||
width: min(100%, 420px);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(67, 84, 118, 0.25);
|
||||
background: #fff;
|
||||
}
|
||||
.org-top-title {
|
||||
background: #1e2f4d;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
padding: 16px 12px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.org-top-members {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
}
|
||||
.org-teams {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.org-team {
|
||||
border: 1px solid rgba(110, 126, 152, 0.25);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(255,255,255,0.95);
|
||||
min-width: 0;
|
||||
}
|
||||
.org-team h4 {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
font-size: 14px;
|
||||
color: #21324e;
|
||||
font-weight: 800;
|
||||
border-bottom: 1px solid rgba(110, 126, 152, 0.2);
|
||||
background: rgba(240, 245, 252, 0.96);
|
||||
}
|
||||
.org-members {
|
||||
padding: 7px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.org-person {
|
||||
border: 1px solid rgba(116, 133, 161, 0.25);
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease;
|
||||
min-width: 0;
|
||||
}
|
||||
.org-person.active {
|
||||
border-color: rgba(15,118,110,0.6);
|
||||
background: rgba(15,118,110,0.11);
|
||||
}
|
||||
.org-person.assigned {
|
||||
border-color: rgba(37,99,235,0.5);
|
||||
background: rgba(37,99,235,0.1);
|
||||
}
|
||||
.org-person strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
color: #15233a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.org-person small {
|
||||
display: block;
|
||||
color: #5a6a86;
|
||||
font-size: 11px;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.mapper {
|
||||
top: 72px;
|
||||
width: min(96vw, 920px);
|
||||
max-height: 58vh;
|
||||
}
|
||||
.viewer-actions {
|
||||
top: 64px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mapper-head strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
.org-top-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
.org-teams {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
canvas.dragging { cursor: grabbing; }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
min-width: 170px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(17,24,39,0.94);
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translate(12px, 12px);
|
||||
transition: opacity 120ms ease;
|
||||
z-index: 3;
|
||||
}
|
||||
.tooltip.visible { opacity: 1; }
|
||||
.tooltip strong { display: block; margin-bottom: 6px; font-size: 14px; }
|
||||
.tooltip div { font-size: 12px; line-height: 1.45; color: rgba(255,255,255,0.82); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="shell">
|
||||
<main class="panel viewer">
|
||||
<div class="viewer-head">
|
||||
<div class="chip" id="scale-chip"></div>
|
||||
<div class="chip" id="hover-chip">chair hover: none</div>
|
||||
</div>
|
||||
<div class="viewer-actions">
|
||||
<button type="button" id="fit-btn">전체 맞춤</button>
|
||||
<button type="button" class="alt" id="clear-btn">선택 지우기</button>
|
||||
</div>
|
||||
<aside class="mapper hidden-off">
|
||||
<div class="mapper-head">
|
||||
<div id="mapper-status">
|
||||
<strong>조직 현황</strong>
|
||||
<span>선택 인원 없음</span>
|
||||
</div>
|
||||
<button type="button" class="alt" id="clear-assign-btn">매칭 초기화</button>
|
||||
</div>
|
||||
<div class="org-chart" id="org-chart"></div>
|
||||
</aside>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./center_chair_people_payload.js"></script>
|
||||
<script>
|
||||
const DATA = window.CHAIR_MAP_DATA;
|
||||
function decodeSegments(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
||||
return new Int32Array(bytes.buffer);
|
||||
}
|
||||
const bgTileRanges = DATA.bgTileRanges;
|
||||
const bgSegValues = decodeSegments(DATA.bgSegsB64);
|
||||
const chairSegValues = decodeSegments(DATA.chairSegsB64);
|
||||
const chairs = DATA.chairs.map(([key, name, kind, start, count]) => ({
|
||||
key, name, kind, start, count
|
||||
}));
|
||||
const meta = DATA.meta;
|
||||
const world = meta.headerBounds;
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const tooltip = document.getElementById("tooltip");
|
||||
const scaleChip = document.getElementById("scale-chip");
|
||||
const hoverChip = document.getElementById("hover-chip");
|
||||
const STORAGE_KEY = "ptc-chair-selection";
|
||||
const PEOPLE_STORAGE_KEY = "ptc-chair-people";
|
||||
const ASSIGN_STORAGE_KEY = "ptc-chair-assignments";
|
||||
const ACTIVE_PERSON_STORAGE_KEY = "ptc-chair-active-person";
|
||||
const clearAssignBtn = document.getElementById("clear-assign-btn");
|
||||
const orgChartEl = document.getElementById("org-chart");
|
||||
const mapperStatus = document.getElementById("mapper-status");
|
||||
// Prevent stale auto-highlights from previous sessions.
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
localStorage.removeItem(ASSIGN_STORAGE_KEY);
|
||||
const placed = new Set();
|
||||
let people = JSON.parse(localStorage.getItem(PEOPLE_STORAGE_KEY) || "[]");
|
||||
let chairAssignments = {};
|
||||
let activePersonId = null;
|
||||
const ORG_TEMPLATE = {
|
||||
top: {
|
||||
name: "총괄기획실",
|
||||
count: 53,
|
||||
members: [
|
||||
{ name: "장종찬", dept: "총괄기획실", title: "기획실장" },
|
||||
{ name: "김원식", dept: "총괄기획실", title: "전무이사" },
|
||||
],
|
||||
},
|
||||
teams: [
|
||||
{ name: "경영기획팀", count: 6, members: ["김우진", "임민정", "국혜린", "최선아", "김윤재", "이미영"] },
|
||||
{ name: "인재성장팀", count: 5, members: ["조태희", "최근혜", "류원준", "주안기", "정성호"] },
|
||||
{ name: "ERP 기획팀", count: 5, members: ["류호성", "문형식", "최요제", "황대일", "이채봉"] },
|
||||
{ name: "디자인기획팀", count: 17, members: ["신혜영", "정은혜", "김태식", "최예은", "채선영", "최영환", "윤봄이", "이예진", "허유나", "마희연", "김수현", "박지영", "권순호", "정두휘", "김정석", "정지윤", "양숙영"] },
|
||||
{ name: "기술기획팀", count: 11, members: ["김원기", "홍아름", "이경민", "김혜인", "황동환", "최찬호", "이태훈", "김신지", "조찬영", "김용연", "한치영"] },
|
||||
{ name: "협업증진팀", count: 3, members: ["성형일", "박주한", "한승민"] },
|
||||
{ name: "솔루션통합팀", count: 4, members: ["권혁진", "염승호", "윤준수", "김지영"] },
|
||||
],
|
||||
};
|
||||
const chairGeometry = chairs.map((chair) => {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
const path = new Path2D();
|
||||
const hitSegments = new Float32Array(chair.count * 4);
|
||||
let segCursor = 0;
|
||||
for (let i = chair.start; i < chair.start + chair.count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = chairSegValues[offset] / 10;
|
||||
const y1 = chairSegValues[offset + 1] / 10;
|
||||
const x2 = chairSegValues[offset + 2] / 10;
|
||||
const y2 = chairSegValues[offset + 3] / 10;
|
||||
path.moveTo(x1, y1);
|
||||
path.lineTo(x2, y2);
|
||||
hitSegments[segCursor] = x1;
|
||||
hitSegments[segCursor + 1] = y1;
|
||||
hitSegments[segCursor + 2] = x2;
|
||||
hitSegments[segCursor + 3] = y2;
|
||||
segCursor += 4;
|
||||
minX = Math.min(minX, x1, x2);
|
||||
minY = Math.min(minY, y1, y2);
|
||||
maxX = Math.max(maxX, x1, x2);
|
||||
maxY = Math.max(maxY, y1, y2);
|
||||
}
|
||||
return {
|
||||
...chair,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
area: Math.max(1, (maxX - minX) * (maxY - minY)),
|
||||
path,
|
||||
hitSegments,
|
||||
};
|
||||
});
|
||||
function renumberChairKeys(chairItems) {
|
||||
if (!chairItems.length) return;
|
||||
const heights = chairItems
|
||||
.map((chair) => Math.max(1, chair.maxY - chair.minY))
|
||||
.sort((a, b) => a - b);
|
||||
const medianHeight = heights[Math.floor(heights.length / 2)] || 1;
|
||||
const rowTolerance = Math.max(40, medianHeight * 0.9);
|
||||
|
||||
const sorted = [...chairItems].sort((a, b) => {
|
||||
const ay = (a.minY + a.maxY) * 0.5;
|
||||
const by = (b.minY + b.maxY) * 0.5;
|
||||
if (Math.abs(by - ay) > rowTolerance) return by - ay; // top -> bottom
|
||||
const ax = (a.minX + a.maxX) * 0.5;
|
||||
const bx = (b.minX + b.maxX) * 0.5;
|
||||
return ax - bx; // left -> right
|
||||
});
|
||||
|
||||
sorted.forEach((chair, index) => {
|
||||
chair.key = String(index + 1);
|
||||
chair.seatNo = index + 1;
|
||||
});
|
||||
}
|
||||
renumberChairKeys(chairGeometry);
|
||||
const PICK_GRID_SIZE = 1800;
|
||||
const chairPickGrid = new Map();
|
||||
function pickGridKey(gx, gy) {
|
||||
return `${gx},${gy}`;
|
||||
}
|
||||
chairGeometry.forEach((chair, index) => {
|
||||
const minGX = Math.floor(chair.minX / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor(chair.maxX / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor(chair.minY / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor(chair.maxY / PICK_GRID_SIZE);
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const key = pickGridKey(gx, gy);
|
||||
if (!chairPickGrid.has(key)) chairPickGrid.set(key, []);
|
||||
chairPickGrid.get(key).push(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const camera = { scale: 1, offsetX: 0, offsetY: 0 };
|
||||
let pixelRatio = window.devicePixelRatio || 1;
|
||||
let pointer = { x: 0, y: 0 };
|
||||
let dragging = false;
|
||||
let dragStart = null;
|
||||
let hovered = null;
|
||||
let rafPending = false;
|
||||
|
||||
function normalizePeople(raw) {
|
||||
return raw
|
||||
.map((person, index) => {
|
||||
if (!person || !person.name) return null;
|
||||
return {
|
||||
id: person.id || `person-${index + 1}`,
|
||||
name: String(person.name).trim(),
|
||||
dept: String(person.dept || "").trim(),
|
||||
title: String(person.title || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function createTemplatePeople() {
|
||||
const generated = [];
|
||||
let seq = 1;
|
||||
ORG_TEMPLATE.top.members.forEach((member) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name: member.name,
|
||||
dept: member.dept,
|
||||
title: member.title,
|
||||
});
|
||||
});
|
||||
ORG_TEMPLATE.teams.forEach((team) => {
|
||||
team.members.forEach((name) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name,
|
||||
dept: team.name,
|
||||
title: "선임",
|
||||
});
|
||||
});
|
||||
});
|
||||
return generated;
|
||||
}
|
||||
|
||||
people = normalizePeople(people);
|
||||
const templateReady = people.some((person) => person.name === "장종찬" && person.dept === "총괄기획실");
|
||||
if (!templateReady) {
|
||||
people = createTemplatePeople();
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
const chairKeySet = new Set(chairGeometry.map((chair) => chair.key));
|
||||
chairAssignments = Object.fromEntries(
|
||||
Object.entries(chairAssignments).filter(([chairKey, personId]) => (
|
||||
chairKeySet.has(chairKey) && people.some((person) => person.id === personId)
|
||||
))
|
||||
);
|
||||
if (activePersonId && !people.some((person) => person.id === activePersonId)) activePersonId = null;
|
||||
|
||||
function persistPeople() {
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
|
||||
function persistAssignments() {
|
||||
localStorage.setItem(ASSIGN_STORAGE_KEY, JSON.stringify(chairAssignments));
|
||||
}
|
||||
|
||||
function persistActivePerson() {
|
||||
if (!activePersonId) localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
else localStorage.setItem(ACTIVE_PERSON_STORAGE_KEY, activePersonId);
|
||||
}
|
||||
|
||||
function assignmentCount() {
|
||||
return Object.keys(chairAssignments).length;
|
||||
}
|
||||
|
||||
function getPersonById(id) {
|
||||
return people.find((person) => person.id === id) || null;
|
||||
}
|
||||
|
||||
function getChairByPerson(personId) {
|
||||
for (const [chairKey, assignedPersonId] of Object.entries(chairAssignments)) {
|
||||
if (assignedPersonId === personId) return chairKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderPeopleList() {
|
||||
const activePerson = getPersonById(activePersonId);
|
||||
const countText = `${assignmentCount()} / ${people.length} 매칭`;
|
||||
mapperStatus.innerHTML = `<strong>조직 현황</strong><span>${activePerson ? `${activePerson.name} 선택됨` : "선택 인원 없음"} · ${countText}</span>`;
|
||||
|
||||
const findPerson = (dept, name) => people.find((person) => person.dept === dept && person.name === name) || null;
|
||||
const personCard = (person, roleText) => {
|
||||
if (!person) return "";
|
||||
const chairKey = getChairByPerson(person.id);
|
||||
const assignedClass = chairKey ? " assigned" : "";
|
||||
const activeClass = person.id === activePersonId ? " active" : "";
|
||||
return `
|
||||
<article class="org-person${assignedClass}${activeClass}" data-person-id="${person.id}">
|
||||
<strong>${person.name}</strong>
|
||||
<small>${person.title || roleText || "-"}</small>
|
||||
<small>${chairKey ? `좌석 ${chairKey}` : "좌석 미지정"}</small>
|
||||
</article>
|
||||
`;
|
||||
};
|
||||
|
||||
const topHtml = ORG_TEMPLATE.top.members
|
||||
.map((member) => personCard(findPerson(member.dept, member.name), member.title))
|
||||
.join("");
|
||||
|
||||
const teamsHtml = ORG_TEMPLATE.teams.map((team) => {
|
||||
const membersHtml = team.members
|
||||
.map((name) => personCard(findPerson(team.name, name), "선임"))
|
||||
.join("");
|
||||
return `
|
||||
<section class="org-team">
|
||||
<h4>${team.name} (${team.count})</h4>
|
||||
<div class="org-members">${membersHtml}</div>
|
||||
</section>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
orgChartEl.innerHTML = `
|
||||
<section class="org-top">
|
||||
<div class="org-top-title">${ORG_TEMPLATE.top.name} (${ORG_TEMPLATE.top.count})</div>
|
||||
<div class="org-top-members">${topHtml}</div>
|
||||
</section>
|
||||
<section class="org-teams">${teamsHtml}</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function worldToScreen(x, y) {
|
||||
return {
|
||||
x: x * camera.scale + camera.offsetX,
|
||||
y: (world.maxY - y + world.minY) * camera.scale + camera.offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
function screenToWorld(x, y) {
|
||||
return {
|
||||
x: (x - camera.offsetX) / camera.scale,
|
||||
y: world.maxY + world.minY - (y - camera.offsetY) / camera.scale,
|
||||
};
|
||||
}
|
||||
|
||||
function resize() {
|
||||
pixelRatio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.round(rect.width * pixelRatio);
|
||||
canvas.height = Math.round(rect.height * pixelRatio);
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
fit();
|
||||
}
|
||||
|
||||
function fit() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const width = world.maxX - world.minX;
|
||||
const height = world.maxY - world.minY;
|
||||
const pad = 36;
|
||||
const scaleX = (rect.width - pad * 2) / width;
|
||||
const scaleY = (rect.height - pad * 2) / height;
|
||||
camera.scale = Math.min(scaleX, scaleY);
|
||||
camera.offsetX = pad - world.minX * camera.scale + (rect.width - pad * 2 - width * camera.scale) / 2;
|
||||
camera.offsetY = pad - world.minY * camera.scale + (rect.height - pad * 2 - height * camera.scale) / 2;
|
||||
requestDraw();
|
||||
}
|
||||
|
||||
function drawGrid(width, height) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(21,35,48,0.05)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 120; x < width; x += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 120; y < height; y += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function pickChair(screenX, screenY) {
|
||||
const threshold = 12;
|
||||
const pointerWorld = screenToWorld(screenX, screenY);
|
||||
const thresholdWorld = threshold / camera.scale;
|
||||
const thresholdWorldSq = thresholdWorld * thresholdWorld;
|
||||
const minGX = Math.floor((pointerWorld.x - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor((pointerWorld.x + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor((pointerWorld.y - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor((pointerWorld.y + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const candidateIndexes = [];
|
||||
const seen = new Set();
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const candidates = chairPickGrid.get(pickGridKey(gx, gy));
|
||||
if (!candidates) continue;
|
||||
for (const index of candidates) {
|
||||
if (seen.has(index)) continue;
|
||||
seen.add(index);
|
||||
candidateIndexes.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
let best = null;
|
||||
for (const index of candidateIndexes) {
|
||||
const chair = chairGeometry[index];
|
||||
if (
|
||||
pointerWorld.x < chair.minX - thresholdWorld ||
|
||||
pointerWorld.x > chair.maxX + thresholdWorld ||
|
||||
pointerWorld.y < chair.minY - thresholdWorld ||
|
||||
pointerWorld.y > chair.maxY + thresholdWorld
|
||||
) continue;
|
||||
let distSq = Infinity;
|
||||
for (let i = 0; i < chair.hitSegments.length; i += 4) {
|
||||
const x1 = chair.hitSegments[i];
|
||||
const y1 = chair.hitSegments[i + 1];
|
||||
const x2 = chair.hitSegments[i + 2];
|
||||
const y2 = chair.hitSegments[i + 3];
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
let segDistSq;
|
||||
if (len2 === 0) {
|
||||
const px = pointerWorld.x - x1;
|
||||
const py = pointerWorld.y - y1;
|
||||
segDistSq = px * px + py * py;
|
||||
} else {
|
||||
let t = ((pointerWorld.x - x1) * dx + (pointerWorld.y - y1) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const lx = x1 + t * dx;
|
||||
const ly = y1 + t * dy;
|
||||
const px = pointerWorld.x - lx;
|
||||
const py = pointerWorld.y - ly;
|
||||
segDistSq = px * px + py * py;
|
||||
}
|
||||
if (segDistSq < distSq) distSq = segDistSq;
|
||||
if (distSq <= thresholdWorldSq * 0.3) break;
|
||||
}
|
||||
if (distSq > thresholdWorldSq) continue;
|
||||
const dist = Math.sqrt(distSq) * camera.scale;
|
||||
|
||||
if (!best) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
const distGap = dist - best.dist;
|
||||
if (distGap < -0.75) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Math.abs(distGap) <= 2) {
|
||||
const areaGap = chair.area - best.chair.area;
|
||||
if (areaGap < -1) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(areaGap) <= 1 &&
|
||||
chair.kind === "block" &&
|
||||
best.chair.kind !== "block"
|
||||
) {
|
||||
best = { chair, dist };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best ? best.chair : null;
|
||||
}
|
||||
|
||||
function renderTooltip() {
|
||||
if (!hovered) {
|
||||
tooltip.classList.remove("visible");
|
||||
hoverChip.textContent = "chair hover: none";
|
||||
return;
|
||||
}
|
||||
hoverChip.textContent = `chair hover: ${hovered.name}`;
|
||||
tooltip.innerHTML = `
|
||||
<strong>${hovered.name}</strong>
|
||||
<div>chair key: ${hovered.key}</div>
|
||||
<div>${placed.has(hovered.key) ? "선택됨" : "클릭하면 선택"}</div>
|
||||
<div>${chairAssignments[hovered.key] ? `배치: ${(getPersonById(chairAssignments[hovered.key]) || { name: "알수없음" }).name}` : "배치 인원 없음"}</div>
|
||||
`;
|
||||
tooltip.style.left = `${pointer.x + 14}px`;
|
||||
tooltip.style.top = `${pointer.y + 14}px`;
|
||||
tooltip.classList.add("visible");
|
||||
}
|
||||
|
||||
function requestDraw() {
|
||||
if (rafPending) return;
|
||||
rafPending = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
rafPending = false;
|
||||
draw();
|
||||
});
|
||||
}
|
||||
|
||||
function applyWorldTransform() {
|
||||
ctx.setTransform(
|
||||
pixelRatio * camera.scale,
|
||||
0,
|
||||
0,
|
||||
-pixelRatio * camera.scale,
|
||||
pixelRatio * camera.offsetX,
|
||||
pixelRatio * ((world.maxY + world.minY) * camera.scale + camera.offsetY)
|
||||
);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
drawGrid(rect.width, rect.height);
|
||||
const viewA = screenToWorld(0, rect.height);
|
||||
const viewB = screenToWorld(rect.width, 0);
|
||||
const viewMinX = Math.min(viewA.x, viewB.x);
|
||||
const viewMaxX = Math.max(viewA.x, viewB.x);
|
||||
const viewMinY = Math.min(viewA.y, viewB.y);
|
||||
const viewMaxY = Math.max(viewA.y, viewB.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.strokeStyle = "rgba(100, 116, 139, 0.28)";
|
||||
ctx.lineWidth = 1 / camera.scale;
|
||||
const tileSize = meta.backgroundTileSize;
|
||||
const tileMinX = Math.floor(viewMinX / tileSize);
|
||||
const tileMaxX = Math.floor(viewMaxX / tileSize);
|
||||
const tileMinY = Math.floor(viewMinY / tileSize);
|
||||
const tileMaxY = Math.floor(viewMaxY / tileSize);
|
||||
for (let tx = tileMinX; tx <= tileMaxX; tx += 1) {
|
||||
for (let ty = tileMinY; ty <= tileMaxY; ty += 1) {
|
||||
const range = bgTileRanges[`${tx},${ty}`];
|
||||
if (!range) continue;
|
||||
const start = range[0];
|
||||
const count = range[1];
|
||||
for (let i = start; i < start + count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = bgSegValues[offset] / 10;
|
||||
const y1 = bgSegValues[offset + 1] / 10;
|
||||
const x2 = bgSegValues[offset + 2] / 10;
|
||||
const y2 = bgSegValues[offset + 3] / 10;
|
||||
if (
|
||||
Math.max(x1, x2) < viewMinX ||
|
||||
Math.min(x1, x2) > viewMaxX ||
|
||||
Math.max(y1, y2) < viewMinY ||
|
||||
Math.min(y1, y2) > viewMaxY
|
||||
) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
hovered = dragging ? null : pickChair(pointer.x, pointer.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.lineWidth = 1.45 / camera.scale;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
for (const chair of chairGeometry) {
|
||||
if (chair.maxX < viewMinX || chair.minX > viewMaxX || chair.maxY < viewMinY || chair.minY > viewMaxY) continue;
|
||||
const active = hovered && hovered.key === chair.key;
|
||||
const selected = placed.has(chair.key);
|
||||
const assignedPersonId = chairAssignments[chair.key];
|
||||
const activePersonChair = activePersonId && assignedPersonId === activePersonId;
|
||||
const assigned = Boolean(assignedPersonId);
|
||||
const baseWidth = chair.kind === "block" ? 1.45 : 1.35;
|
||||
ctx.strokeStyle = activePersonChair
|
||||
? "rgba(234, 179, 8, 1)"
|
||||
: assigned
|
||||
? "rgba(37, 99, 235, 0.98)"
|
||||
: selected
|
||||
? "rgba(220, 38, 38, 0.98)"
|
||||
: active
|
||||
? "rgba(15, 118, 110, 0.98)"
|
||||
: chair.kind === "group"
|
||||
? "rgba(16, 134, 149, 0.74)"
|
||||
: "rgba(21, 149, 142, 0.8)";
|
||||
ctx.lineWidth = (activePersonChair ? 2.8 : assigned ? 2.4 : selected ? 2.6 : active ? 2.1 : baseWidth) / camera.scale;
|
||||
ctx.stroke(chair.path);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
scaleChip.textContent = `scale ${camera.scale.toFixed(4)}x`;
|
||||
renderTooltip();
|
||||
}
|
||||
|
||||
function persistPlaced() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify([...placed]));
|
||||
}
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
dragging = true;
|
||||
dragStart = { x: event.clientX, y: event.clientY, offsetX: camera.offsetX, offsetY: camera.offsetY };
|
||||
canvas.classList.add("dragging");
|
||||
});
|
||||
|
||||
window.addEventListener("pointerup", (event) => {
|
||||
if (dragging && dragStart) {
|
||||
const move = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
|
||||
if (move < 4) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const picked = pickChair(event.clientX - rect.left, event.clientY - rect.top);
|
||||
if (picked) {
|
||||
if (placed.has(picked.key)) placed.delete(picked.key);
|
||||
else placed.add(picked.key);
|
||||
persistPlaced();
|
||||
if (activePersonId) {
|
||||
const currentChair = getChairByPerson(activePersonId);
|
||||
if (chairAssignments[picked.key] === activePersonId) {
|
||||
delete chairAssignments[picked.key];
|
||||
} else {
|
||||
if (currentChair && currentChair !== picked.key) delete chairAssignments[currentChair];
|
||||
chairAssignments[picked.key] = activePersonId;
|
||||
}
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dragging = false;
|
||||
dragStart = null;
|
||||
canvas.classList.remove("dragging");
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
window.addEventListener("pointermove", (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
||||
if (dragging && dragStart) {
|
||||
camera.offsetX = dragStart.offsetX + (event.clientX - dragStart.x);
|
||||
camera.offsetY = dragStart.offsetY + (event.clientY - dragStart.y);
|
||||
}
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = event.clientX - rect.left;
|
||||
const my = event.clientY - rect.top;
|
||||
const before = screenToWorld(mx, my);
|
||||
const factor = event.deltaY < 0 ? 1.08 : 0.92;
|
||||
camera.scale = Math.max(0.002, Math.min(2, camera.scale * factor));
|
||||
const after = worldToScreen(before.x, before.y);
|
||||
camera.offsetX += mx - after.x;
|
||||
camera.offsetY += my - after.y;
|
||||
requestDraw();
|
||||
}, { passive: false });
|
||||
|
||||
document.getElementById("fit-btn").addEventListener("click", fit);
|
||||
document.getElementById("clear-btn").addEventListener("click", () => {
|
||||
placed.clear();
|
||||
persistPlaced();
|
||||
requestDraw();
|
||||
});
|
||||
clearAssignBtn.addEventListener("click", () => {
|
||||
chairAssignments = {};
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
orgChartEl.addEventListener("click", (event) => {
|
||||
const item = event.target.closest(".org-person[data-person-id]");
|
||||
if (!item) return;
|
||||
const personId = item.getAttribute("data-person-id");
|
||||
activePersonId = personId === activePersonId ? null : personId;
|
||||
persistActivePerson();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
window.addEventListener("resize", resize);
|
||||
renderPeopleList();
|
||||
resize();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
932
incoming-files/seat/center_chair_people_map_6f.html
Normal file
932
incoming-files/seat/center_chair_people_map_6f.html
Normal file
@@ -0,0 +1,932 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>center chair people map 6f</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #152330;
|
||||
--muted: #627286;
|
||||
--paper: rgba(255,255,255,0.86);
|
||||
--line: rgba(21,35,48,0.1);
|
||||
--accent: #0f766e;
|
||||
--bg: #edf2f6;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "IBM Plex Sans KR", "Pretendard", sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(15,118,110,0.11), transparent 22%),
|
||||
linear-gradient(180deg, #f5f8fb 0%, #e8eef3 100%);
|
||||
}
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 10px 14px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #0f766e, #115e59);
|
||||
box-shadow: 0 10px 22px rgba(15,118,110,0.18);
|
||||
}
|
||||
button.alt {
|
||||
color: var(--ink);
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: none;
|
||||
}
|
||||
.viewer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.viewer-head {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chip {
|
||||
padding: 10px 12px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.82);
|
||||
border: 1px solid rgba(255,255,255,0.94);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 24px rgba(21,35,48,0.08);
|
||||
}
|
||||
.viewer-actions {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 64px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.mapper {
|
||||
position: absolute;
|
||||
top: 76px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(94vw, 1320px);
|
||||
max-height: min(56vh, 560px);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
border-radius: 20px;
|
||||
background: rgba(234, 239, 247, 0.95);
|
||||
border: 1px solid rgba(101, 119, 146, 0.22);
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.hidden-off {
|
||||
display: none !important;
|
||||
}
|
||||
.mapper-head {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid rgba(101,119,146,0.18);
|
||||
font-size: 12px;
|
||||
color: #51607a;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
.mapper-head strong {
|
||||
display: block;
|
||||
color: #17243b;
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.mapper-head .alt {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.org-chart {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.org-top {
|
||||
margin: 0 auto;
|
||||
width: min(100%, 420px);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(67, 84, 118, 0.25);
|
||||
background: #fff;
|
||||
}
|
||||
.org-top-title {
|
||||
background: #1e2f4d;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
padding: 16px 12px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.org-top-members {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
}
|
||||
.org-teams {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.org-team {
|
||||
border: 1px solid rgba(110, 126, 152, 0.25);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(255,255,255,0.95);
|
||||
min-width: 0;
|
||||
}
|
||||
.org-team h4 {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
font-size: 14px;
|
||||
color: #21324e;
|
||||
font-weight: 800;
|
||||
border-bottom: 1px solid rgba(110, 126, 152, 0.2);
|
||||
background: rgba(240, 245, 252, 0.96);
|
||||
}
|
||||
.org-members {
|
||||
padding: 7px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.org-person {
|
||||
border: 1px solid rgba(116, 133, 161, 0.25);
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease;
|
||||
min-width: 0;
|
||||
}
|
||||
.org-person.active {
|
||||
border-color: rgba(15,118,110,0.6);
|
||||
background: rgba(15,118,110,0.11);
|
||||
}
|
||||
.org-person.assigned {
|
||||
border-color: rgba(37,99,235,0.5);
|
||||
background: rgba(37,99,235,0.1);
|
||||
}
|
||||
.org-person strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
color: #15233a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.org-person small {
|
||||
display: block;
|
||||
color: #5a6a86;
|
||||
font-size: 11px;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.mapper {
|
||||
top: 72px;
|
||||
width: min(96vw, 920px);
|
||||
max-height: 58vh;
|
||||
}
|
||||
.viewer-actions {
|
||||
top: 64px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mapper-head strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
.org-top-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
.org-teams {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
canvas.dragging { cursor: grabbing; }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
min-width: 170px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(17,24,39,0.94);
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translate(12px, 12px);
|
||||
transition: opacity 120ms ease;
|
||||
z-index: 3;
|
||||
}
|
||||
.tooltip.visible { opacity: 1; }
|
||||
.tooltip strong { display: block; margin-bottom: 6px; font-size: 14px; }
|
||||
.tooltip div { font-size: 12px; line-height: 1.45; color: rgba(255,255,255,0.82); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="shell">
|
||||
<main class="panel viewer">
|
||||
<div class="viewer-head">
|
||||
<div class="chip" id="scale-chip"></div>
|
||||
<div class="chip" id="hover-chip">chair hover: none</div>
|
||||
</div>
|
||||
<div class="viewer-actions">
|
||||
<button type="button" id="fit-btn">전체 맞춤</button>
|
||||
<button type="button" class="alt" id="clear-btn">선택 지우기</button>
|
||||
</div>
|
||||
<aside class="mapper hidden-off">
|
||||
<div class="mapper-head">
|
||||
<div id="mapper-status">
|
||||
<strong>조직 현황</strong>
|
||||
<span>선택 인원 없음</span>
|
||||
</div>
|
||||
<button type="button" class="alt" id="clear-assign-btn">매칭 초기화</button>
|
||||
</div>
|
||||
<div class="org-chart" id="org-chart"></div>
|
||||
</aside>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./center_chair_people_payload_6f.js"></script>
|
||||
<script>
|
||||
const DATA = window.CHAIR_MAP_DATA;
|
||||
function decodeSegments(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
||||
return new Int32Array(bytes.buffer);
|
||||
}
|
||||
const bgTileRanges = DATA.bgTileRanges;
|
||||
const bgSegValues = decodeSegments(DATA.bgSegsB64);
|
||||
const chairSegValues = decodeSegments(DATA.chairSegsB64);
|
||||
const chairs = DATA.chairs.map(([key, name, kind, start, count]) => ({
|
||||
key, name, kind, start, count
|
||||
}));
|
||||
const meta = DATA.meta;
|
||||
const world = meta.headerBounds;
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const tooltip = document.getElementById("tooltip");
|
||||
const scaleChip = document.getElementById("scale-chip");
|
||||
const hoverChip = document.getElementById("hover-chip");
|
||||
const STORAGE_KEY = "ptc-chair-selection";
|
||||
const PEOPLE_STORAGE_KEY = "ptc-chair-people";
|
||||
const ASSIGN_STORAGE_KEY = "ptc-chair-assignments";
|
||||
const ACTIVE_PERSON_STORAGE_KEY = "ptc-chair-active-person";
|
||||
const clearAssignBtn = document.getElementById("clear-assign-btn");
|
||||
const orgChartEl = document.getElementById("org-chart");
|
||||
const mapperStatus = document.getElementById("mapper-status");
|
||||
// Prevent stale auto-highlights from previous sessions.
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
localStorage.removeItem(ASSIGN_STORAGE_KEY);
|
||||
const placed = new Set();
|
||||
let people = JSON.parse(localStorage.getItem(PEOPLE_STORAGE_KEY) || "[]");
|
||||
let chairAssignments = {};
|
||||
let activePersonId = null;
|
||||
const ORG_TEMPLATE = {
|
||||
top: {
|
||||
name: "총괄기획실",
|
||||
count: 53,
|
||||
members: [
|
||||
{ name: "장종찬", dept: "총괄기획실", title: "기획실장" },
|
||||
{ name: "김원식", dept: "총괄기획실", title: "전무이사" },
|
||||
],
|
||||
},
|
||||
teams: [
|
||||
{ name: "경영기획팀", count: 6, members: ["김우진", "임민정", "국혜린", "최선아", "김윤재", "이미영"] },
|
||||
{ name: "인재성장팀", count: 5, members: ["조태희", "최근혜", "류원준", "주안기", "정성호"] },
|
||||
{ name: "ERP 기획팀", count: 5, members: ["류호성", "문형식", "최요제", "황대일", "이채봉"] },
|
||||
{ name: "디자인기획팀", count: 17, members: ["신혜영", "정은혜", "김태식", "최예은", "채선영", "최영환", "윤봄이", "이예진", "허유나", "마희연", "김수현", "박지영", "권순호", "정두휘", "김정석", "정지윤", "양숙영"] },
|
||||
{ name: "기술기획팀", count: 11, members: ["김원기", "홍아름", "이경민", "김혜인", "황동환", "최찬호", "이태훈", "김신지", "조찬영", "김용연", "한치영"] },
|
||||
{ name: "협업증진팀", count: 3, members: ["성형일", "박주한", "한승민"] },
|
||||
{ name: "솔루션통합팀", count: 4, members: ["권혁진", "염승호", "윤준수", "김지영"] },
|
||||
],
|
||||
};
|
||||
const chairGeometry = chairs.map((chair) => {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
const path = new Path2D();
|
||||
const hitSegments = new Float32Array(chair.count * 4);
|
||||
let segCursor = 0;
|
||||
for (let i = chair.start; i < chair.start + chair.count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = chairSegValues[offset] / 10;
|
||||
const y1 = chairSegValues[offset + 1] / 10;
|
||||
const x2 = chairSegValues[offset + 2] / 10;
|
||||
const y2 = chairSegValues[offset + 3] / 10;
|
||||
path.moveTo(x1, y1);
|
||||
path.lineTo(x2, y2);
|
||||
hitSegments[segCursor] = x1;
|
||||
hitSegments[segCursor + 1] = y1;
|
||||
hitSegments[segCursor + 2] = x2;
|
||||
hitSegments[segCursor + 3] = y2;
|
||||
segCursor += 4;
|
||||
minX = Math.min(minX, x1, x2);
|
||||
minY = Math.min(minY, y1, y2);
|
||||
maxX = Math.max(maxX, x1, x2);
|
||||
maxY = Math.max(maxY, y1, y2);
|
||||
}
|
||||
return {
|
||||
...chair,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
area: Math.max(1, (maxX - minX) * (maxY - minY)),
|
||||
path,
|
||||
hitSegments,
|
||||
};
|
||||
});
|
||||
function renumberChairKeys(chairItems) {
|
||||
if (!chairItems.length) return;
|
||||
const heights = chairItems
|
||||
.map((chair) => Math.max(1, chair.maxY - chair.minY))
|
||||
.sort((a, b) => a - b);
|
||||
const medianHeight = heights[Math.floor(heights.length / 2)] || 1;
|
||||
const rowTolerance = Math.max(40, medianHeight * 0.9);
|
||||
|
||||
const sorted = [...chairItems].sort((a, b) => {
|
||||
const ay = (a.minY + a.maxY) * 0.5;
|
||||
const by = (b.minY + b.maxY) * 0.5;
|
||||
if (Math.abs(by - ay) > rowTolerance) return by - ay; // top -> bottom
|
||||
const ax = (a.minX + a.maxX) * 0.5;
|
||||
const bx = (b.minX + b.maxX) * 0.5;
|
||||
return ax - bx; // left -> right
|
||||
});
|
||||
|
||||
sorted.forEach((chair, index) => {
|
||||
chair.key = String(index + 1);
|
||||
chair.seatNo = index + 1;
|
||||
});
|
||||
}
|
||||
renumberChairKeys(chairGeometry);
|
||||
const PICK_GRID_SIZE = 1800;
|
||||
const chairPickGrid = new Map();
|
||||
function pickGridKey(gx, gy) {
|
||||
return `${gx},${gy}`;
|
||||
}
|
||||
chairGeometry.forEach((chair, index) => {
|
||||
const minGX = Math.floor(chair.minX / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor(chair.maxX / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor(chair.minY / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor(chair.maxY / PICK_GRID_SIZE);
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const key = pickGridKey(gx, gy);
|
||||
if (!chairPickGrid.has(key)) chairPickGrid.set(key, []);
|
||||
chairPickGrid.get(key).push(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const camera = { scale: 1, offsetX: 0, offsetY: 0 };
|
||||
let pixelRatio = window.devicePixelRatio || 1;
|
||||
let pointer = { x: 0, y: 0 };
|
||||
let dragging = false;
|
||||
let dragStart = null;
|
||||
let hovered = null;
|
||||
let rafPending = false;
|
||||
|
||||
function normalizePeople(raw) {
|
||||
return raw
|
||||
.map((person, index) => {
|
||||
if (!person || !person.name) return null;
|
||||
return {
|
||||
id: person.id || `person-${index + 1}`,
|
||||
name: String(person.name).trim(),
|
||||
dept: String(person.dept || "").trim(),
|
||||
title: String(person.title || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function createTemplatePeople() {
|
||||
const generated = [];
|
||||
let seq = 1;
|
||||
ORG_TEMPLATE.top.members.forEach((member) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name: member.name,
|
||||
dept: member.dept,
|
||||
title: member.title,
|
||||
});
|
||||
});
|
||||
ORG_TEMPLATE.teams.forEach((team) => {
|
||||
team.members.forEach((name) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name,
|
||||
dept: team.name,
|
||||
title: "선임",
|
||||
});
|
||||
});
|
||||
});
|
||||
return generated;
|
||||
}
|
||||
|
||||
people = normalizePeople(people);
|
||||
const templateReady = people.some((person) => person.name === "장종찬" && person.dept === "총괄기획실");
|
||||
if (!templateReady) {
|
||||
people = createTemplatePeople();
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
const chairKeySet = new Set(chairGeometry.map((chair) => chair.key));
|
||||
chairAssignments = Object.fromEntries(
|
||||
Object.entries(chairAssignments).filter(([chairKey, personId]) => (
|
||||
chairKeySet.has(chairKey) && people.some((person) => person.id === personId)
|
||||
))
|
||||
);
|
||||
if (activePersonId && !people.some((person) => person.id === activePersonId)) activePersonId = null;
|
||||
|
||||
function persistPeople() {
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
|
||||
function persistAssignments() {
|
||||
localStorage.setItem(ASSIGN_STORAGE_KEY, JSON.stringify(chairAssignments));
|
||||
}
|
||||
|
||||
function persistActivePerson() {
|
||||
if (!activePersonId) localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
else localStorage.setItem(ACTIVE_PERSON_STORAGE_KEY, activePersonId);
|
||||
}
|
||||
|
||||
function assignmentCount() {
|
||||
return Object.keys(chairAssignments).length;
|
||||
}
|
||||
|
||||
function getPersonById(id) {
|
||||
return people.find((person) => person.id === id) || null;
|
||||
}
|
||||
|
||||
function getChairByPerson(personId) {
|
||||
for (const [chairKey, assignedPersonId] of Object.entries(chairAssignments)) {
|
||||
if (assignedPersonId === personId) return chairKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderPeopleList() {
|
||||
const activePerson = getPersonById(activePersonId);
|
||||
const countText = `${assignmentCount()} / ${people.length} 매칭`;
|
||||
mapperStatus.innerHTML = `<strong>조직 현황</strong><span>${activePerson ? `${activePerson.name} 선택됨` : "선택 인원 없음"} · ${countText}</span>`;
|
||||
|
||||
const findPerson = (dept, name) => people.find((person) => person.dept === dept && person.name === name) || null;
|
||||
const personCard = (person, roleText) => {
|
||||
if (!person) return "";
|
||||
const chairKey = getChairByPerson(person.id);
|
||||
const assignedClass = chairKey ? " assigned" : "";
|
||||
const activeClass = person.id === activePersonId ? " active" : "";
|
||||
return `
|
||||
<article class="org-person${assignedClass}${activeClass}" data-person-id="${person.id}">
|
||||
<strong>${person.name}</strong>
|
||||
<small>${person.title || roleText || "-"}</small>
|
||||
<small>${chairKey ? `좌석 ${chairKey}` : "좌석 미지정"}</small>
|
||||
</article>
|
||||
`;
|
||||
};
|
||||
|
||||
const topHtml = ORG_TEMPLATE.top.members
|
||||
.map((member) => personCard(findPerson(member.dept, member.name), member.title))
|
||||
.join("");
|
||||
|
||||
const teamsHtml = ORG_TEMPLATE.teams.map((team) => {
|
||||
const membersHtml = team.members
|
||||
.map((name) => personCard(findPerson(team.name, name), "선임"))
|
||||
.join("");
|
||||
return `
|
||||
<section class="org-team">
|
||||
<h4>${team.name} (${team.count})</h4>
|
||||
<div class="org-members">${membersHtml}</div>
|
||||
</section>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
orgChartEl.innerHTML = `
|
||||
<section class="org-top">
|
||||
<div class="org-top-title">${ORG_TEMPLATE.top.name} (${ORG_TEMPLATE.top.count})</div>
|
||||
<div class="org-top-members">${topHtml}</div>
|
||||
</section>
|
||||
<section class="org-teams">${teamsHtml}</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function worldToScreen(x, y) {
|
||||
return {
|
||||
x: x * camera.scale + camera.offsetX,
|
||||
y: (world.maxY - y + world.minY) * camera.scale + camera.offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
function screenToWorld(x, y) {
|
||||
return {
|
||||
x: (x - camera.offsetX) / camera.scale,
|
||||
y: world.maxY + world.minY - (y - camera.offsetY) / camera.scale,
|
||||
};
|
||||
}
|
||||
|
||||
function resize() {
|
||||
pixelRatio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.round(rect.width * pixelRatio);
|
||||
canvas.height = Math.round(rect.height * pixelRatio);
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
fit();
|
||||
}
|
||||
|
||||
function fit() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const width = world.maxX - world.minX;
|
||||
const height = world.maxY - world.minY;
|
||||
const pad = 36;
|
||||
const scaleX = (rect.width - pad * 2) / width;
|
||||
const scaleY = (rect.height - pad * 2) / height;
|
||||
camera.scale = Math.min(scaleX, scaleY);
|
||||
camera.offsetX = pad - world.minX * camera.scale + (rect.width - pad * 2 - width * camera.scale) / 2;
|
||||
camera.offsetY = pad - world.minY * camera.scale + (rect.height - pad * 2 - height * camera.scale) / 2;
|
||||
requestDraw();
|
||||
}
|
||||
|
||||
function drawGrid(width, height) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(21,35,48,0.05)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 120; x < width; x += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 120; y < height; y += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function pickChair(screenX, screenY) {
|
||||
const threshold = 12;
|
||||
const pointerWorld = screenToWorld(screenX, screenY);
|
||||
const thresholdWorld = threshold / camera.scale;
|
||||
const thresholdWorldSq = thresholdWorld * thresholdWorld;
|
||||
const minGX = Math.floor((pointerWorld.x - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor((pointerWorld.x + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor((pointerWorld.y - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor((pointerWorld.y + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const candidateIndexes = [];
|
||||
const seen = new Set();
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const candidates = chairPickGrid.get(pickGridKey(gx, gy));
|
||||
if (!candidates) continue;
|
||||
for (const index of candidates) {
|
||||
if (seen.has(index)) continue;
|
||||
seen.add(index);
|
||||
candidateIndexes.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
let best = null;
|
||||
for (const index of candidateIndexes) {
|
||||
const chair = chairGeometry[index];
|
||||
if (
|
||||
pointerWorld.x < chair.minX - thresholdWorld ||
|
||||
pointerWorld.x > chair.maxX + thresholdWorld ||
|
||||
pointerWorld.y < chair.minY - thresholdWorld ||
|
||||
pointerWorld.y > chair.maxY + thresholdWorld
|
||||
) continue;
|
||||
let distSq = Infinity;
|
||||
for (let i = 0; i < chair.hitSegments.length; i += 4) {
|
||||
const x1 = chair.hitSegments[i];
|
||||
const y1 = chair.hitSegments[i + 1];
|
||||
const x2 = chair.hitSegments[i + 2];
|
||||
const y2 = chair.hitSegments[i + 3];
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
let segDistSq;
|
||||
if (len2 === 0) {
|
||||
const px = pointerWorld.x - x1;
|
||||
const py = pointerWorld.y - y1;
|
||||
segDistSq = px * px + py * py;
|
||||
} else {
|
||||
let t = ((pointerWorld.x - x1) * dx + (pointerWorld.y - y1) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const lx = x1 + t * dx;
|
||||
const ly = y1 + t * dy;
|
||||
const px = pointerWorld.x - lx;
|
||||
const py = pointerWorld.y - ly;
|
||||
segDistSq = px * px + py * py;
|
||||
}
|
||||
if (segDistSq < distSq) distSq = segDistSq;
|
||||
if (distSq <= thresholdWorldSq * 0.3) break;
|
||||
}
|
||||
if (distSq > thresholdWorldSq) continue;
|
||||
const dist = Math.sqrt(distSq) * camera.scale;
|
||||
|
||||
if (!best) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
const distGap = dist - best.dist;
|
||||
if (distGap < -0.75) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Math.abs(distGap) <= 2) {
|
||||
const areaGap = chair.area - best.chair.area;
|
||||
if (areaGap < -1) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(areaGap) <= 1 &&
|
||||
chair.kind === "block" &&
|
||||
best.chair.kind !== "block"
|
||||
) {
|
||||
best = { chair, dist };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best ? best.chair : null;
|
||||
}
|
||||
|
||||
function renderTooltip() {
|
||||
if (!hovered) {
|
||||
tooltip.classList.remove("visible");
|
||||
hoverChip.textContent = "chair hover: none";
|
||||
return;
|
||||
}
|
||||
hoverChip.textContent = `chair hover: ${hovered.name}`;
|
||||
tooltip.innerHTML = `
|
||||
<strong>${hovered.name}</strong>
|
||||
<div>chair key: ${hovered.key}</div>
|
||||
<div>${placed.has(hovered.key) ? "선택됨" : "클릭하면 선택"}</div>
|
||||
<div>${chairAssignments[hovered.key] ? `배치: ${(getPersonById(chairAssignments[hovered.key]) || { name: "알수없음" }).name}` : "배치 인원 없음"}</div>
|
||||
`;
|
||||
tooltip.style.left = `${pointer.x + 14}px`;
|
||||
tooltip.style.top = `${pointer.y + 14}px`;
|
||||
tooltip.classList.add("visible");
|
||||
}
|
||||
|
||||
function requestDraw() {
|
||||
if (rafPending) return;
|
||||
rafPending = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
rafPending = false;
|
||||
draw();
|
||||
});
|
||||
}
|
||||
|
||||
function applyWorldTransform() {
|
||||
ctx.setTransform(
|
||||
pixelRatio * camera.scale,
|
||||
0,
|
||||
0,
|
||||
-pixelRatio * camera.scale,
|
||||
pixelRatio * camera.offsetX,
|
||||
pixelRatio * ((world.maxY + world.minY) * camera.scale + camera.offsetY)
|
||||
);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
drawGrid(rect.width, rect.height);
|
||||
const viewA = screenToWorld(0, rect.height);
|
||||
const viewB = screenToWorld(rect.width, 0);
|
||||
const viewMinX = Math.min(viewA.x, viewB.x);
|
||||
const viewMaxX = Math.max(viewA.x, viewB.x);
|
||||
const viewMinY = Math.min(viewA.y, viewB.y);
|
||||
const viewMaxY = Math.max(viewA.y, viewB.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.strokeStyle = "rgba(100, 116, 139, 0.28)";
|
||||
ctx.lineWidth = 1 / camera.scale;
|
||||
const tileSize = meta.backgroundTileSize;
|
||||
const tileMinX = Math.floor(viewMinX / tileSize);
|
||||
const tileMaxX = Math.floor(viewMaxX / tileSize);
|
||||
const tileMinY = Math.floor(viewMinY / tileSize);
|
||||
const tileMaxY = Math.floor(viewMaxY / tileSize);
|
||||
for (let tx = tileMinX; tx <= tileMaxX; tx += 1) {
|
||||
for (let ty = tileMinY; ty <= tileMaxY; ty += 1) {
|
||||
const range = bgTileRanges[`${tx},${ty}`];
|
||||
if (!range) continue;
|
||||
const start = range[0];
|
||||
const count = range[1];
|
||||
for (let i = start; i < start + count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = bgSegValues[offset] / 10;
|
||||
const y1 = bgSegValues[offset + 1] / 10;
|
||||
const x2 = bgSegValues[offset + 2] / 10;
|
||||
const y2 = bgSegValues[offset + 3] / 10;
|
||||
if (
|
||||
Math.max(x1, x2) < viewMinX ||
|
||||
Math.min(x1, x2) > viewMaxX ||
|
||||
Math.max(y1, y2) < viewMinY ||
|
||||
Math.min(y1, y2) > viewMaxY
|
||||
) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
hovered = dragging ? null : pickChair(pointer.x, pointer.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.lineWidth = 1.45 / camera.scale;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
for (const chair of chairGeometry) {
|
||||
if (chair.maxX < viewMinX || chair.minX > viewMaxX || chair.maxY < viewMinY || chair.minY > viewMaxY) continue;
|
||||
const active = hovered && hovered.key === chair.key;
|
||||
const selected = placed.has(chair.key);
|
||||
const assignedPersonId = chairAssignments[chair.key];
|
||||
const activePersonChair = activePersonId && assignedPersonId === activePersonId;
|
||||
const assigned = Boolean(assignedPersonId);
|
||||
const baseWidth = chair.kind === "block" ? 1.45 : 1.35;
|
||||
ctx.strokeStyle = activePersonChair
|
||||
? "rgba(234, 179, 8, 1)"
|
||||
: assigned
|
||||
? "rgba(37, 99, 235, 0.98)"
|
||||
: selected
|
||||
? "rgba(220, 38, 38, 0.98)"
|
||||
: active
|
||||
? "rgba(15, 118, 110, 0.98)"
|
||||
: chair.kind === "group"
|
||||
? "rgba(16, 134, 149, 0.74)"
|
||||
: "rgba(21, 149, 142, 0.8)";
|
||||
ctx.lineWidth = (activePersonChair ? 2.8 : assigned ? 2.4 : selected ? 2.6 : active ? 2.1 : baseWidth) / camera.scale;
|
||||
ctx.stroke(chair.path);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
scaleChip.textContent = `scale ${camera.scale.toFixed(4)}x`;
|
||||
renderTooltip();
|
||||
}
|
||||
|
||||
function persistPlaced() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify([...placed]));
|
||||
}
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
dragging = true;
|
||||
dragStart = { x: event.clientX, y: event.clientY, offsetX: camera.offsetX, offsetY: camera.offsetY };
|
||||
canvas.classList.add("dragging");
|
||||
});
|
||||
|
||||
window.addEventListener("pointerup", (event) => {
|
||||
if (dragging && dragStart) {
|
||||
const move = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
|
||||
if (move < 4) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const picked = pickChair(event.clientX - rect.left, event.clientY - rect.top);
|
||||
if (picked) {
|
||||
if (placed.has(picked.key)) placed.delete(picked.key);
|
||||
else placed.add(picked.key);
|
||||
persistPlaced();
|
||||
if (activePersonId) {
|
||||
const currentChair = getChairByPerson(activePersonId);
|
||||
if (chairAssignments[picked.key] === activePersonId) {
|
||||
delete chairAssignments[picked.key];
|
||||
} else {
|
||||
if (currentChair && currentChair !== picked.key) delete chairAssignments[currentChair];
|
||||
chairAssignments[picked.key] = activePersonId;
|
||||
}
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dragging = false;
|
||||
dragStart = null;
|
||||
canvas.classList.remove("dragging");
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
window.addEventListener("pointermove", (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
||||
if (dragging && dragStart) {
|
||||
camera.offsetX = dragStart.offsetX + (event.clientX - dragStart.x);
|
||||
camera.offsetY = dragStart.offsetY + (event.clientY - dragStart.y);
|
||||
}
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = event.clientX - rect.left;
|
||||
const my = event.clientY - rect.top;
|
||||
const before = screenToWorld(mx, my);
|
||||
const factor = event.deltaY < 0 ? 1.08 : 0.92;
|
||||
camera.scale = Math.max(0.002, Math.min(2, camera.scale * factor));
|
||||
const after = worldToScreen(before.x, before.y);
|
||||
camera.offsetX += mx - after.x;
|
||||
camera.offsetY += my - after.y;
|
||||
requestDraw();
|
||||
}, { passive: false });
|
||||
|
||||
document.getElementById("fit-btn").addEventListener("click", fit);
|
||||
document.getElementById("clear-btn").addEventListener("click", () => {
|
||||
placed.clear();
|
||||
persistPlaced();
|
||||
requestDraw();
|
||||
});
|
||||
clearAssignBtn.addEventListener("click", () => {
|
||||
chairAssignments = {};
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
orgChartEl.addEventListener("click", (event) => {
|
||||
const item = event.target.closest(".org-person[data-person-id]");
|
||||
if (!item) return;
|
||||
const personId = item.getAttribute("data-person-id");
|
||||
activePersonId = personId === activePersonId ? null : personId;
|
||||
persistActivePerson();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
window.addEventListener("resize", resize);
|
||||
renderPeopleList();
|
||||
resize();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
932
incoming-files/seat/center_chair_people_map_7f.html
Normal file
932
incoming-files/seat/center_chair_people_map_7f.html
Normal file
@@ -0,0 +1,932 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>center chair people map 7f</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #152330;
|
||||
--muted: #627286;
|
||||
--paper: rgba(255,255,255,0.86);
|
||||
--line: rgba(21,35,48,0.1);
|
||||
--accent: #0f766e;
|
||||
--bg: #edf2f6;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "IBM Plex Sans KR", "Pretendard", sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(15,118,110,0.11), transparent 22%),
|
||||
linear-gradient(180deg, #f5f8fb 0%, #e8eef3 100%);
|
||||
}
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 10px 14px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #0f766e, #115e59);
|
||||
box-shadow: 0 10px 22px rgba(15,118,110,0.18);
|
||||
}
|
||||
button.alt {
|
||||
color: var(--ink);
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: none;
|
||||
}
|
||||
.viewer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.viewer-head {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chip {
|
||||
padding: 10px 12px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.82);
|
||||
border: 1px solid rgba(255,255,255,0.94);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 24px rgba(21,35,48,0.08);
|
||||
}
|
||||
.viewer-actions {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 64px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.mapper {
|
||||
position: absolute;
|
||||
top: 76px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(94vw, 1320px);
|
||||
max-height: min(56vh, 560px);
|
||||
overflow: hidden;
|
||||
z-index: 4;
|
||||
border-radius: 20px;
|
||||
background: rgba(234, 239, 247, 0.95);
|
||||
border: 1px solid rgba(101, 119, 146, 0.22);
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.hidden-off {
|
||||
display: none !important;
|
||||
}
|
||||
.mapper-head {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid rgba(101,119,146,0.18);
|
||||
font-size: 12px;
|
||||
color: #51607a;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
.mapper-head strong {
|
||||
display: block;
|
||||
color: #17243b;
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.mapper-head .alt {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.org-chart {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.org-top {
|
||||
margin: 0 auto;
|
||||
width: min(100%, 420px);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(67, 84, 118, 0.25);
|
||||
background: #fff;
|
||||
}
|
||||
.org-top-title {
|
||||
background: #1e2f4d;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
padding: 16px 12px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.org-top-members {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
}
|
||||
.org-teams {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.org-team {
|
||||
border: 1px solid rgba(110, 126, 152, 0.25);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(255,255,255,0.95);
|
||||
min-width: 0;
|
||||
}
|
||||
.org-team h4 {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
font-size: 14px;
|
||||
color: #21324e;
|
||||
font-weight: 800;
|
||||
border-bottom: 1px solid rgba(110, 126, 152, 0.2);
|
||||
background: rgba(240, 245, 252, 0.96);
|
||||
}
|
||||
.org-members {
|
||||
padding: 7px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.org-person {
|
||||
border: 1px solid rgba(116, 133, 161, 0.25);
|
||||
background: rgba(255,255,255,0.95);
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease;
|
||||
min-width: 0;
|
||||
}
|
||||
.org-person.active {
|
||||
border-color: rgba(15,118,110,0.6);
|
||||
background: rgba(15,118,110,0.11);
|
||||
}
|
||||
.org-person.assigned {
|
||||
border-color: rgba(37,99,235,0.5);
|
||||
background: rgba(37,99,235,0.1);
|
||||
}
|
||||
.org-person strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
color: #15233a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.org-person small {
|
||||
display: block;
|
||||
color: #5a6a86;
|
||||
font-size: 11px;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.mapper {
|
||||
top: 72px;
|
||||
width: min(96vw, 920px);
|
||||
max-height: 58vh;
|
||||
}
|
||||
.viewer-actions {
|
||||
top: 64px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mapper-head strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
.org-top-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
.org-teams {
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
canvas.dragging { cursor: grabbing; }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
min-width: 170px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(17,24,39,0.94);
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translate(12px, 12px);
|
||||
transition: opacity 120ms ease;
|
||||
z-index: 3;
|
||||
}
|
||||
.tooltip.visible { opacity: 1; }
|
||||
.tooltip strong { display: block; margin-bottom: 6px; font-size: 14px; }
|
||||
.tooltip div { font-size: 12px; line-height: 1.45; color: rgba(255,255,255,0.82); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="shell">
|
||||
<main class="panel viewer">
|
||||
<div class="viewer-head">
|
||||
<div class="chip" id="scale-chip"></div>
|
||||
<div class="chip" id="hover-chip">chair hover: none</div>
|
||||
</div>
|
||||
<div class="viewer-actions">
|
||||
<button type="button" id="fit-btn">전체 맞춤</button>
|
||||
<button type="button" class="alt" id="clear-btn">선택 지우기</button>
|
||||
</div>
|
||||
<aside class="mapper hidden-off">
|
||||
<div class="mapper-head">
|
||||
<div id="mapper-status">
|
||||
<strong>조직 현황</strong>
|
||||
<span>선택 인원 없음</span>
|
||||
</div>
|
||||
<button type="button" class="alt" id="clear-assign-btn">매칭 초기화</button>
|
||||
</div>
|
||||
<div class="org-chart" id="org-chart"></div>
|
||||
</aside>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./center_chair_people_payload_7f.js"></script>
|
||||
<script>
|
||||
const DATA = window.CHAIR_MAP_DATA;
|
||||
function decodeSegments(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
||||
return new Int32Array(bytes.buffer);
|
||||
}
|
||||
const bgTileRanges = DATA.bgTileRanges;
|
||||
const bgSegValues = decodeSegments(DATA.bgSegsB64);
|
||||
const chairSegValues = decodeSegments(DATA.chairSegsB64);
|
||||
const chairs = DATA.chairs.map(([key, name, kind, start, count]) => ({
|
||||
key, name, kind, start, count
|
||||
}));
|
||||
const meta = DATA.meta;
|
||||
const world = meta.headerBounds;
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const tooltip = document.getElementById("tooltip");
|
||||
const scaleChip = document.getElementById("scale-chip");
|
||||
const hoverChip = document.getElementById("hover-chip");
|
||||
const STORAGE_KEY = "ptc-chair-selection";
|
||||
const PEOPLE_STORAGE_KEY = "ptc-chair-people";
|
||||
const ASSIGN_STORAGE_KEY = "ptc-chair-assignments";
|
||||
const ACTIVE_PERSON_STORAGE_KEY = "ptc-chair-active-person";
|
||||
const clearAssignBtn = document.getElementById("clear-assign-btn");
|
||||
const orgChartEl = document.getElementById("org-chart");
|
||||
const mapperStatus = document.getElementById("mapper-status");
|
||||
// Prevent stale auto-highlights from previous sessions.
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
localStorage.removeItem(ASSIGN_STORAGE_KEY);
|
||||
const placed = new Set();
|
||||
let people = JSON.parse(localStorage.getItem(PEOPLE_STORAGE_KEY) || "[]");
|
||||
let chairAssignments = {};
|
||||
let activePersonId = null;
|
||||
const ORG_TEMPLATE = {
|
||||
top: {
|
||||
name: "총괄기획실",
|
||||
count: 53,
|
||||
members: [
|
||||
{ name: "장종찬", dept: "총괄기획실", title: "기획실장" },
|
||||
{ name: "김원식", dept: "총괄기획실", title: "전무이사" },
|
||||
],
|
||||
},
|
||||
teams: [
|
||||
{ name: "경영기획팀", count: 6, members: ["김우진", "임민정", "국혜린", "최선아", "김윤재", "이미영"] },
|
||||
{ name: "인재성장팀", count: 5, members: ["조태희", "최근혜", "류원준", "주안기", "정성호"] },
|
||||
{ name: "ERP 기획팀", count: 5, members: ["류호성", "문형식", "최요제", "황대일", "이채봉"] },
|
||||
{ name: "디자인기획팀", count: 17, members: ["신혜영", "정은혜", "김태식", "최예은", "채선영", "최영환", "윤봄이", "이예진", "허유나", "마희연", "김수현", "박지영", "권순호", "정두휘", "김정석", "정지윤", "양숙영"] },
|
||||
{ name: "기술기획팀", count: 11, members: ["김원기", "홍아름", "이경민", "김혜인", "황동환", "최찬호", "이태훈", "김신지", "조찬영", "김용연", "한치영"] },
|
||||
{ name: "협업증진팀", count: 3, members: ["성형일", "박주한", "한승민"] },
|
||||
{ name: "솔루션통합팀", count: 4, members: ["권혁진", "염승호", "윤준수", "김지영"] },
|
||||
],
|
||||
};
|
||||
const chairGeometry = chairs.map((chair) => {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
const path = new Path2D();
|
||||
const hitSegments = new Float32Array(chair.count * 4);
|
||||
let segCursor = 0;
|
||||
for (let i = chair.start; i < chair.start + chair.count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = chairSegValues[offset] / 10;
|
||||
const y1 = chairSegValues[offset + 1] / 10;
|
||||
const x2 = chairSegValues[offset + 2] / 10;
|
||||
const y2 = chairSegValues[offset + 3] / 10;
|
||||
path.moveTo(x1, y1);
|
||||
path.lineTo(x2, y2);
|
||||
hitSegments[segCursor] = x1;
|
||||
hitSegments[segCursor + 1] = y1;
|
||||
hitSegments[segCursor + 2] = x2;
|
||||
hitSegments[segCursor + 3] = y2;
|
||||
segCursor += 4;
|
||||
minX = Math.min(minX, x1, x2);
|
||||
minY = Math.min(minY, y1, y2);
|
||||
maxX = Math.max(maxX, x1, x2);
|
||||
maxY = Math.max(maxY, y1, y2);
|
||||
}
|
||||
return {
|
||||
...chair,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
area: Math.max(1, (maxX - minX) * (maxY - minY)),
|
||||
path,
|
||||
hitSegments,
|
||||
};
|
||||
});
|
||||
function renumberChairKeys(chairItems) {
|
||||
if (!chairItems.length) return;
|
||||
const heights = chairItems
|
||||
.map((chair) => Math.max(1, chair.maxY - chair.minY))
|
||||
.sort((a, b) => a - b);
|
||||
const medianHeight = heights[Math.floor(heights.length / 2)] || 1;
|
||||
const rowTolerance = Math.max(40, medianHeight * 0.9);
|
||||
|
||||
const sorted = [...chairItems].sort((a, b) => {
|
||||
const ay = (a.minY + a.maxY) * 0.5;
|
||||
const by = (b.minY + b.maxY) * 0.5;
|
||||
if (Math.abs(by - ay) > rowTolerance) return by - ay; // top -> bottom
|
||||
const ax = (a.minX + a.maxX) * 0.5;
|
||||
const bx = (b.minX + b.maxX) * 0.5;
|
||||
return ax - bx; // left -> right
|
||||
});
|
||||
|
||||
sorted.forEach((chair, index) => {
|
||||
chair.key = String(index + 1);
|
||||
chair.seatNo = index + 1;
|
||||
});
|
||||
}
|
||||
renumberChairKeys(chairGeometry);
|
||||
const PICK_GRID_SIZE = 1800;
|
||||
const chairPickGrid = new Map();
|
||||
function pickGridKey(gx, gy) {
|
||||
return `${gx},${gy}`;
|
||||
}
|
||||
chairGeometry.forEach((chair, index) => {
|
||||
const minGX = Math.floor(chair.minX / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor(chair.maxX / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor(chair.minY / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor(chair.maxY / PICK_GRID_SIZE);
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const key = pickGridKey(gx, gy);
|
||||
if (!chairPickGrid.has(key)) chairPickGrid.set(key, []);
|
||||
chairPickGrid.get(key).push(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const camera = { scale: 1, offsetX: 0, offsetY: 0 };
|
||||
let pixelRatio = window.devicePixelRatio || 1;
|
||||
let pointer = { x: 0, y: 0 };
|
||||
let dragging = false;
|
||||
let dragStart = null;
|
||||
let hovered = null;
|
||||
let rafPending = false;
|
||||
|
||||
function normalizePeople(raw) {
|
||||
return raw
|
||||
.map((person, index) => {
|
||||
if (!person || !person.name) return null;
|
||||
return {
|
||||
id: person.id || `person-${index + 1}`,
|
||||
name: String(person.name).trim(),
|
||||
dept: String(person.dept || "").trim(),
|
||||
title: String(person.title || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function createTemplatePeople() {
|
||||
const generated = [];
|
||||
let seq = 1;
|
||||
ORG_TEMPLATE.top.members.forEach((member) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name: member.name,
|
||||
dept: member.dept,
|
||||
title: member.title,
|
||||
});
|
||||
});
|
||||
ORG_TEMPLATE.teams.forEach((team) => {
|
||||
team.members.forEach((name) => {
|
||||
generated.push({
|
||||
id: `org-${seq++}`,
|
||||
name,
|
||||
dept: team.name,
|
||||
title: "선임",
|
||||
});
|
||||
});
|
||||
});
|
||||
return generated;
|
||||
}
|
||||
|
||||
people = normalizePeople(people);
|
||||
const templateReady = people.some((person) => person.name === "장종찬" && person.dept === "총괄기획실");
|
||||
if (!templateReady) {
|
||||
people = createTemplatePeople();
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
const chairKeySet = new Set(chairGeometry.map((chair) => chair.key));
|
||||
chairAssignments = Object.fromEntries(
|
||||
Object.entries(chairAssignments).filter(([chairKey, personId]) => (
|
||||
chairKeySet.has(chairKey) && people.some((person) => person.id === personId)
|
||||
))
|
||||
);
|
||||
if (activePersonId && !people.some((person) => person.id === activePersonId)) activePersonId = null;
|
||||
|
||||
function persistPeople() {
|
||||
localStorage.setItem(PEOPLE_STORAGE_KEY, JSON.stringify(people));
|
||||
}
|
||||
|
||||
function persistAssignments() {
|
||||
localStorage.setItem(ASSIGN_STORAGE_KEY, JSON.stringify(chairAssignments));
|
||||
}
|
||||
|
||||
function persistActivePerson() {
|
||||
if (!activePersonId) localStorage.removeItem(ACTIVE_PERSON_STORAGE_KEY);
|
||||
else localStorage.setItem(ACTIVE_PERSON_STORAGE_KEY, activePersonId);
|
||||
}
|
||||
|
||||
function assignmentCount() {
|
||||
return Object.keys(chairAssignments).length;
|
||||
}
|
||||
|
||||
function getPersonById(id) {
|
||||
return people.find((person) => person.id === id) || null;
|
||||
}
|
||||
|
||||
function getChairByPerson(personId) {
|
||||
for (const [chairKey, assignedPersonId] of Object.entries(chairAssignments)) {
|
||||
if (assignedPersonId === personId) return chairKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderPeopleList() {
|
||||
const activePerson = getPersonById(activePersonId);
|
||||
const countText = `${assignmentCount()} / ${people.length} 매칭`;
|
||||
mapperStatus.innerHTML = `<strong>조직 현황</strong><span>${activePerson ? `${activePerson.name} 선택됨` : "선택 인원 없음"} · ${countText}</span>`;
|
||||
|
||||
const findPerson = (dept, name) => people.find((person) => person.dept === dept && person.name === name) || null;
|
||||
const personCard = (person, roleText) => {
|
||||
if (!person) return "";
|
||||
const chairKey = getChairByPerson(person.id);
|
||||
const assignedClass = chairKey ? " assigned" : "";
|
||||
const activeClass = person.id === activePersonId ? " active" : "";
|
||||
return `
|
||||
<article class="org-person${assignedClass}${activeClass}" data-person-id="${person.id}">
|
||||
<strong>${person.name}</strong>
|
||||
<small>${person.title || roleText || "-"}</small>
|
||||
<small>${chairKey ? `좌석 ${chairKey}` : "좌석 미지정"}</small>
|
||||
</article>
|
||||
`;
|
||||
};
|
||||
|
||||
const topHtml = ORG_TEMPLATE.top.members
|
||||
.map((member) => personCard(findPerson(member.dept, member.name), member.title))
|
||||
.join("");
|
||||
|
||||
const teamsHtml = ORG_TEMPLATE.teams.map((team) => {
|
||||
const membersHtml = team.members
|
||||
.map((name) => personCard(findPerson(team.name, name), "선임"))
|
||||
.join("");
|
||||
return `
|
||||
<section class="org-team">
|
||||
<h4>${team.name} (${team.count})</h4>
|
||||
<div class="org-members">${membersHtml}</div>
|
||||
</section>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
orgChartEl.innerHTML = `
|
||||
<section class="org-top">
|
||||
<div class="org-top-title">${ORG_TEMPLATE.top.name} (${ORG_TEMPLATE.top.count})</div>
|
||||
<div class="org-top-members">${topHtml}</div>
|
||||
</section>
|
||||
<section class="org-teams">${teamsHtml}</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function worldToScreen(x, y) {
|
||||
return {
|
||||
x: x * camera.scale + camera.offsetX,
|
||||
y: (world.maxY - y + world.minY) * camera.scale + camera.offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
function screenToWorld(x, y) {
|
||||
return {
|
||||
x: (x - camera.offsetX) / camera.scale,
|
||||
y: world.maxY + world.minY - (y - camera.offsetY) / camera.scale,
|
||||
};
|
||||
}
|
||||
|
||||
function resize() {
|
||||
pixelRatio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.round(rect.width * pixelRatio);
|
||||
canvas.height = Math.round(rect.height * pixelRatio);
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
fit();
|
||||
}
|
||||
|
||||
function fit() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const width = world.maxX - world.minX;
|
||||
const height = world.maxY - world.minY;
|
||||
const pad = 36;
|
||||
const scaleX = (rect.width - pad * 2) / width;
|
||||
const scaleY = (rect.height - pad * 2) / height;
|
||||
camera.scale = Math.min(scaleX, scaleY);
|
||||
camera.offsetX = pad - world.minX * camera.scale + (rect.width - pad * 2 - width * camera.scale) / 2;
|
||||
camera.offsetY = pad - world.minY * camera.scale + (rect.height - pad * 2 - height * camera.scale) / 2;
|
||||
requestDraw();
|
||||
}
|
||||
|
||||
function drawGrid(width, height) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(21,35,48,0.05)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 120; x < width; x += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 120; y < height; y += 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function pickChair(screenX, screenY) {
|
||||
const threshold = 12;
|
||||
const pointerWorld = screenToWorld(screenX, screenY);
|
||||
const thresholdWorld = threshold / camera.scale;
|
||||
const thresholdWorldSq = thresholdWorld * thresholdWorld;
|
||||
const minGX = Math.floor((pointerWorld.x - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGX = Math.floor((pointerWorld.x + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const minGY = Math.floor((pointerWorld.y - thresholdWorld) / PICK_GRID_SIZE);
|
||||
const maxGY = Math.floor((pointerWorld.y + thresholdWorld) / PICK_GRID_SIZE);
|
||||
const candidateIndexes = [];
|
||||
const seen = new Set();
|
||||
for (let gx = minGX; gx <= maxGX; gx += 1) {
|
||||
for (let gy = minGY; gy <= maxGY; gy += 1) {
|
||||
const candidates = chairPickGrid.get(pickGridKey(gx, gy));
|
||||
if (!candidates) continue;
|
||||
for (const index of candidates) {
|
||||
if (seen.has(index)) continue;
|
||||
seen.add(index);
|
||||
candidateIndexes.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
let best = null;
|
||||
for (const index of candidateIndexes) {
|
||||
const chair = chairGeometry[index];
|
||||
if (
|
||||
pointerWorld.x < chair.minX - thresholdWorld ||
|
||||
pointerWorld.x > chair.maxX + thresholdWorld ||
|
||||
pointerWorld.y < chair.minY - thresholdWorld ||
|
||||
pointerWorld.y > chair.maxY + thresholdWorld
|
||||
) continue;
|
||||
let distSq = Infinity;
|
||||
for (let i = 0; i < chair.hitSegments.length; i += 4) {
|
||||
const x1 = chair.hitSegments[i];
|
||||
const y1 = chair.hitSegments[i + 1];
|
||||
const x2 = chair.hitSegments[i + 2];
|
||||
const y2 = chair.hitSegments[i + 3];
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
let segDistSq;
|
||||
if (len2 === 0) {
|
||||
const px = pointerWorld.x - x1;
|
||||
const py = pointerWorld.y - y1;
|
||||
segDistSq = px * px + py * py;
|
||||
} else {
|
||||
let t = ((pointerWorld.x - x1) * dx + (pointerWorld.y - y1) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const lx = x1 + t * dx;
|
||||
const ly = y1 + t * dy;
|
||||
const px = pointerWorld.x - lx;
|
||||
const py = pointerWorld.y - ly;
|
||||
segDistSq = px * px + py * py;
|
||||
}
|
||||
if (segDistSq < distSq) distSq = segDistSq;
|
||||
if (distSq <= thresholdWorldSq * 0.3) break;
|
||||
}
|
||||
if (distSq > thresholdWorldSq) continue;
|
||||
const dist = Math.sqrt(distSq) * camera.scale;
|
||||
|
||||
if (!best) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
const distGap = dist - best.dist;
|
||||
if (distGap < -0.75) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Math.abs(distGap) <= 2) {
|
||||
const areaGap = chair.area - best.chair.area;
|
||||
if (areaGap < -1) {
|
||||
best = { chair, dist };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(areaGap) <= 1 &&
|
||||
chair.kind === "block" &&
|
||||
best.chair.kind !== "block"
|
||||
) {
|
||||
best = { chair, dist };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best ? best.chair : null;
|
||||
}
|
||||
|
||||
function renderTooltip() {
|
||||
if (!hovered) {
|
||||
tooltip.classList.remove("visible");
|
||||
hoverChip.textContent = "chair hover: none";
|
||||
return;
|
||||
}
|
||||
hoverChip.textContent = `chair hover: ${hovered.name}`;
|
||||
tooltip.innerHTML = `
|
||||
<strong>${hovered.name}</strong>
|
||||
<div>chair key: ${hovered.key}</div>
|
||||
<div>${placed.has(hovered.key) ? "선택됨" : "클릭하면 선택"}</div>
|
||||
<div>${chairAssignments[hovered.key] ? `배치: ${(getPersonById(chairAssignments[hovered.key]) || { name: "알수없음" }).name}` : "배치 인원 없음"}</div>
|
||||
`;
|
||||
tooltip.style.left = `${pointer.x + 14}px`;
|
||||
tooltip.style.top = `${pointer.y + 14}px`;
|
||||
tooltip.classList.add("visible");
|
||||
}
|
||||
|
||||
function requestDraw() {
|
||||
if (rafPending) return;
|
||||
rafPending = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
rafPending = false;
|
||||
draw();
|
||||
});
|
||||
}
|
||||
|
||||
function applyWorldTransform() {
|
||||
ctx.setTransform(
|
||||
pixelRatio * camera.scale,
|
||||
0,
|
||||
0,
|
||||
-pixelRatio * camera.scale,
|
||||
pixelRatio * camera.offsetX,
|
||||
pixelRatio * ((world.maxY + world.minY) * camera.scale + camera.offsetY)
|
||||
);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
drawGrid(rect.width, rect.height);
|
||||
const viewA = screenToWorld(0, rect.height);
|
||||
const viewB = screenToWorld(rect.width, 0);
|
||||
const viewMinX = Math.min(viewA.x, viewB.x);
|
||||
const viewMaxX = Math.max(viewA.x, viewB.x);
|
||||
const viewMinY = Math.min(viewA.y, viewB.y);
|
||||
const viewMaxY = Math.max(viewA.y, viewB.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.strokeStyle = "rgba(100, 116, 139, 0.28)";
|
||||
ctx.lineWidth = 1 / camera.scale;
|
||||
const tileSize = meta.backgroundTileSize;
|
||||
const tileMinX = Math.floor(viewMinX / tileSize);
|
||||
const tileMaxX = Math.floor(viewMaxX / tileSize);
|
||||
const tileMinY = Math.floor(viewMinY / tileSize);
|
||||
const tileMaxY = Math.floor(viewMaxY / tileSize);
|
||||
for (let tx = tileMinX; tx <= tileMaxX; tx += 1) {
|
||||
for (let ty = tileMinY; ty <= tileMaxY; ty += 1) {
|
||||
const range = bgTileRanges[`${tx},${ty}`];
|
||||
if (!range) continue;
|
||||
const start = range[0];
|
||||
const count = range[1];
|
||||
for (let i = start; i < start + count; i += 1) {
|
||||
const offset = i * 4;
|
||||
const x1 = bgSegValues[offset] / 10;
|
||||
const y1 = bgSegValues[offset + 1] / 10;
|
||||
const x2 = bgSegValues[offset + 2] / 10;
|
||||
const y2 = bgSegValues[offset + 3] / 10;
|
||||
if (
|
||||
Math.max(x1, x2) < viewMinX ||
|
||||
Math.min(x1, x2) > viewMaxX ||
|
||||
Math.max(y1, y2) < viewMinY ||
|
||||
Math.min(y1, y2) > viewMaxY
|
||||
) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
hovered = dragging ? null : pickChair(pointer.x, pointer.y);
|
||||
|
||||
ctx.save();
|
||||
applyWorldTransform();
|
||||
ctx.lineWidth = 1.45 / camera.scale;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
for (const chair of chairGeometry) {
|
||||
if (chair.maxX < viewMinX || chair.minX > viewMaxX || chair.maxY < viewMinY || chair.minY > viewMaxY) continue;
|
||||
const active = hovered && hovered.key === chair.key;
|
||||
const selected = placed.has(chair.key);
|
||||
const assignedPersonId = chairAssignments[chair.key];
|
||||
const activePersonChair = activePersonId && assignedPersonId === activePersonId;
|
||||
const assigned = Boolean(assignedPersonId);
|
||||
const baseWidth = chair.kind === "block" ? 1.45 : 1.35;
|
||||
ctx.strokeStyle = activePersonChair
|
||||
? "rgba(234, 179, 8, 1)"
|
||||
: assigned
|
||||
? "rgba(37, 99, 235, 0.98)"
|
||||
: selected
|
||||
? "rgba(220, 38, 38, 0.98)"
|
||||
: active
|
||||
? "rgba(15, 118, 110, 0.98)"
|
||||
: chair.kind === "group"
|
||||
? "rgba(16, 134, 149, 0.74)"
|
||||
: "rgba(21, 149, 142, 0.8)";
|
||||
ctx.lineWidth = (activePersonChair ? 2.8 : assigned ? 2.4 : selected ? 2.6 : active ? 2.1 : baseWidth) / camera.scale;
|
||||
ctx.stroke(chair.path);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
scaleChip.textContent = `scale ${camera.scale.toFixed(4)}x`;
|
||||
renderTooltip();
|
||||
}
|
||||
|
||||
function persistPlaced() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify([...placed]));
|
||||
}
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
dragging = true;
|
||||
dragStart = { x: event.clientX, y: event.clientY, offsetX: camera.offsetX, offsetY: camera.offsetY };
|
||||
canvas.classList.add("dragging");
|
||||
});
|
||||
|
||||
window.addEventListener("pointerup", (event) => {
|
||||
if (dragging && dragStart) {
|
||||
const move = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
|
||||
if (move < 4) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const picked = pickChair(event.clientX - rect.left, event.clientY - rect.top);
|
||||
if (picked) {
|
||||
if (placed.has(picked.key)) placed.delete(picked.key);
|
||||
else placed.add(picked.key);
|
||||
persistPlaced();
|
||||
if (activePersonId) {
|
||||
const currentChair = getChairByPerson(activePersonId);
|
||||
if (chairAssignments[picked.key] === activePersonId) {
|
||||
delete chairAssignments[picked.key];
|
||||
} else {
|
||||
if (currentChair && currentChair !== picked.key) delete chairAssignments[currentChair];
|
||||
chairAssignments[picked.key] = activePersonId;
|
||||
}
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dragging = false;
|
||||
dragStart = null;
|
||||
canvas.classList.remove("dragging");
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
window.addEventListener("pointermove", (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
||||
if (dragging && dragStart) {
|
||||
camera.offsetX = dragStart.offsetX + (event.clientX - dragStart.x);
|
||||
camera.offsetY = dragStart.offsetY + (event.clientY - dragStart.y);
|
||||
}
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = event.clientX - rect.left;
|
||||
const my = event.clientY - rect.top;
|
||||
const before = screenToWorld(mx, my);
|
||||
const factor = event.deltaY < 0 ? 1.08 : 0.92;
|
||||
camera.scale = Math.max(0.002, Math.min(2, camera.scale * factor));
|
||||
const after = worldToScreen(before.x, before.y);
|
||||
camera.offsetX += mx - after.x;
|
||||
camera.offsetY += my - after.y;
|
||||
requestDraw();
|
||||
}, { passive: false });
|
||||
|
||||
document.getElementById("fit-btn").addEventListener("click", fit);
|
||||
document.getElementById("clear-btn").addEventListener("click", () => {
|
||||
placed.clear();
|
||||
persistPlaced();
|
||||
requestDraw();
|
||||
});
|
||||
clearAssignBtn.addEventListener("click", () => {
|
||||
chairAssignments = {};
|
||||
persistAssignments();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
orgChartEl.addEventListener("click", (event) => {
|
||||
const item = event.target.closest(".org-person[data-person-id]");
|
||||
if (!item) return;
|
||||
const personId = item.getAttribute("data-person-id");
|
||||
activePersonId = personId === activePersonId ? null : personId;
|
||||
persistActivePerson();
|
||||
renderPeopleList();
|
||||
requestDraw();
|
||||
});
|
||||
window.addEventListener("resize", resize);
|
||||
renderPeopleList();
|
||||
resize();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1
incoming-files/seat/center_chair_people_payload.js
Normal file
1
incoming-files/seat/center_chair_people_payload.js
Normal file
File diff suppressed because one or more lines are too long
1
incoming-files/seat/center_chair_people_payload_6f.js
Normal file
1
incoming-files/seat/center_chair_people_payload_6f.js
Normal file
File diff suppressed because one or more lines are too long
1
incoming-files/seat/center_chair_people_payload_7f.js
Normal file
1
incoming-files/seat/center_chair_people_payload_7f.js
Normal file
File diff suppressed because one or more lines are too long
14
incoming-files/served/README.md
Normal file
14
incoming-files/served/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Served Assets
|
||||
|
||||
이 디렉터리는 `8081`에서 실제 URL 응답으로 직접 서빙되는 integration HTML 파일만 둔다.
|
||||
|
||||
현재 사용 중:
|
||||
|
||||
- `payment.html`
|
||||
- `mh.html`
|
||||
|
||||
규칙:
|
||||
|
||||
- `/integrations/payment` 는 이 디렉터리의 `payment.html`을 읽는다.
|
||||
- `/integrations/mh` 는 이 디렉터리의 `mh.html`을 읽는다.
|
||||
- 원본 참고 파일이나 비교용 파일은 이 디렉터리에 두지 않는다.
|
||||
3472
incoming-files/served/mh.html
Normal file
3472
incoming-files/served/mh.html
Normal file
File diff suppressed because it is too large
Load Diff
1622
incoming-files/served/payment.html
Normal file
1622
incoming-files/served/payment.html
Normal file
File diff suppressed because it is too large
Load Diff
1377
incoming-files/사업관리대장/MH 통합 대시보드_260320.css
Normal file
1377
incoming-files/사업관리대장/MH 통합 대시보드_260320.css
Normal file
File diff suppressed because it is too large
Load Diff
2598
incoming-files/사업관리대장/MH 통합 대시보드_260320.html
Normal file
2598
incoming-files/사업관리대장/MH 통합 대시보드_260320.html
Normal file
File diff suppressed because one or more lines are too long
328
incoming-files/사업관리대장/ledger-override.css
Normal file
328
incoming-files/사업관리대장/ledger-override.css
Normal file
@@ -0,0 +1,328 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.mh-business-theme {
|
||||
overflow-x: hidden;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(214, 138, 58, 0.16), transparent 24%),
|
||||
radial-gradient(circle at top right, rgba(47, 153, 115, 0.10), transparent 20%),
|
||||
linear-gradient(180deg, #f6efe6 0%, #f1eadf 100%);
|
||||
}
|
||||
|
||||
body.mh-business-theme .wrap {
|
||||
width: min(100%, 2000px);
|
||||
max-width: 2000px;
|
||||
margin: 0 auto;
|
||||
padding: 18px 18px 26px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.mh-business-theme .top,
|
||||
body.mh-business-theme .status {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
body.mh-business-theme .business-shell {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-top: 2px;
|
||||
padding: 18px;
|
||||
border-radius: 32px;
|
||||
background:
|
||||
radial-gradient(circle at 16% 14%, rgba(255,255,255,0.05), transparent 18%),
|
||||
radial-gradient(circle at 88% 8%, rgba(255,255,255,0.04), transparent 16%),
|
||||
linear-gradient(145deg, #0b352b 0%, #174e41 52%, #245f50 100%);
|
||||
box-shadow: 0 26px 54px rgba(15, 58, 47, 0.16);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards-toolbar {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 10px 0 2px;
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards-toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards-toolbar-search {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: min(360px, 100%);
|
||||
flex: 1 1 320px;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards-toolbar-search .search {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
background: rgba(255,255,255,0.10);
|
||||
color: #f4efe6;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards-toolbar-search .search::placeholder {
|
||||
color: rgba(244, 239, 230, 0.74);
|
||||
}
|
||||
|
||||
body.mh-business-theme #btnUpload {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards-toolbar-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-year-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 60px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: #f4efe6;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-year-chip.active {
|
||||
background: linear-gradient(180deg, #fff8ee 0%, #f2dec0 100%);
|
||||
color: #0a2a22;
|
||||
border-color: rgba(242, 196, 132, 0.58);
|
||||
box-shadow: 0 12px 28px rgba(10, 42, 34, 0.18);
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
min-height: 98px;
|
||||
padding: 18px 22px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.07) 100%);
|
||||
color: #f4efe6;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 16px 30px rgba(7, 28, 22, 0.14);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip.active {
|
||||
background: linear-gradient(180deg, #fff8ee 0%, #f2dec0 100%);
|
||||
color: #0a2a22;
|
||||
border-color: rgba(242, 196, 132, 0.58);
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip .label {
|
||||
color: rgba(244, 239, 230, 0.78);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip.active .label {
|
||||
color: rgba(10, 42, 34, 0.78);
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip .count {
|
||||
color: #fff7e6;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip.active .count {
|
||||
color: #b86b1f;
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip .meta {
|
||||
color: #f2c484;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body.mh-business-theme .summary-filter-chip.active .meta {
|
||||
color: #7c5a20;
|
||||
}
|
||||
|
||||
body.mh-business-theme .card {
|
||||
grid-column: span 2;
|
||||
min-height: 110px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(217, 197, 168, 0.55);
|
||||
background: linear-gradient(180deg, rgba(255,250,243,0.96) 0%, rgba(248,242,232,0.96) 100%);
|
||||
padding: 18px 20px;
|
||||
box-shadow: 0 18px 32px rgba(15, 58, 47, 0.08);
|
||||
}
|
||||
|
||||
body.mh-business-theme .card.management {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
body.mh-business-theme .card .k {
|
||||
color: #5b6d63;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
body.mh-business-theme .card .v {
|
||||
margin-top: 8px;
|
||||
color: #17392f;
|
||||
font-size: 30px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
body.mh-business-theme .card .n {
|
||||
margin-top: 8px;
|
||||
color: #7b6953;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
body.mh-business-theme .panel {
|
||||
border-radius: 28px;
|
||||
border: 1px solid rgba(217, 197, 168, 0.55);
|
||||
box-shadow: 0 18px 32px rgba(15, 58, 47, 0.08);
|
||||
}
|
||||
|
||||
body.mh-business-theme .table-wrap {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border-radius: 28px;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
body.mh-business-theme .table-vat-note {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.mh-business-theme table {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
table-layout: fixed;
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
}
|
||||
|
||||
body.mh-business-theme thead th {
|
||||
background: #0f352b;
|
||||
color: #fff5e6;
|
||||
border-right: 1px solid rgba(242, 196, 132, 0.2);
|
||||
}
|
||||
|
||||
body.mh-business-theme tbody td {
|
||||
background: rgba(255, 250, 243, 0.96);
|
||||
}
|
||||
|
||||
body.mh-business-theme .group-row td {
|
||||
padding: 12px 14px 10px;
|
||||
background: linear-gradient(180deg, rgba(255, 248, 238, 0.98) 0%, rgba(242, 222, 192, 0.78) 100%);
|
||||
border-top: 1px solid rgba(214, 138, 58, 0.26);
|
||||
border-bottom: 1px solid rgba(217, 197, 168, 0.54);
|
||||
}
|
||||
|
||||
body.mh-business-theme .group-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 250, 243, 0.98);
|
||||
border: 1px solid rgba(214, 138, 58, 0.3);
|
||||
color: #17392f;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
box-shadow: 0 8px 18px rgba(15, 58, 47, 0.08);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.mh-business-theme .group-chip .group-toggle {
|
||||
margin-left: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(242, 196, 132, 0.18);
|
||||
color: #b66e22;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
body.mh-business-theme .project-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #17392f;
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.mh-business-theme .project-link:hover {
|
||||
color: #0f6a55;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
body.mh-business-theme .cards-toolbar-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
body.mh-business-theme .card {
|
||||
grid-column: span 4;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
body.mh-business-theme .wrap {
|
||||
padding: 12px 12px 20px;
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
body.mh-business-theme .card {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
body.mh-business-theme .cards-toolbar-search {
|
||||
margin-left: 0;
|
||||
max-width: none;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
498
incoming-files/사업관리대장/ledger-override.js
Normal file
498
incoming-files/사업관리대장/ledger-override.js
Normal file
@@ -0,0 +1,498 @@
|
||||
(function () {
|
||||
window.__mhLedgerEnhancementLoaded = false;
|
||||
if (typeof S === "undefined" || typeof E === "undefined" || typeof render !== "function") return;
|
||||
window.__mhLedgerEnhancementLoaded = true;
|
||||
if (!S.dashboard) S.dashboard = { year: "", section: "active" };
|
||||
if (!S.collapsedGroups) S.collapsedGroups = {};
|
||||
|
||||
function bgToday() {
|
||||
var now = new Date();
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
}
|
||||
|
||||
function bgParseDate(value) {
|
||||
var text = String(value || "").trim();
|
||||
if (!text) return null;
|
||||
var match = text.match(/(20\d{2})\D?(\d{1,2})\D?(\d{1,2})/);
|
||||
if (match) {
|
||||
var parsed = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
||||
return isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
var fallback = new Date(text);
|
||||
if (isNaN(fallback.getTime())) return null;
|
||||
return new Date(fallback.getFullYear(), fallback.getMonth(), fallback.getDate());
|
||||
}
|
||||
|
||||
function bgYearFromText(value) {
|
||||
var match = String(value || "").trim().match(/(20\d{2})/);
|
||||
return match ? match[1] : "";
|
||||
}
|
||||
|
||||
function bgStartYear(row) {
|
||||
return bgYearFromText(row && row.sDate);
|
||||
}
|
||||
|
||||
function bgEndYear(row) {
|
||||
return bgYearFromText(row && row.eDate);
|
||||
}
|
||||
|
||||
function bgDisplayYear(row) {
|
||||
var start = bgStartYear(row);
|
||||
if (start) return start;
|
||||
var contractMatch = String((row && row.cDate) || "").trim().match(/(20\d{2})/);
|
||||
if (contractMatch) return contractMatch[1];
|
||||
var nameMatch = String((row && row.name) || "").trim().match(/^(20\d{2})/);
|
||||
if (nameMatch) return nameMatch[1];
|
||||
return bgEndYear(row) || "미지정";
|
||||
}
|
||||
|
||||
function bgCompletionYear(row) {
|
||||
return bgEndYear(row) || bgDisplayYear(row);
|
||||
}
|
||||
|
||||
function bgDateOrYearStart(row) {
|
||||
var yearText = bgDisplayYear(row);
|
||||
return bgParseDate(row && row.sDate) || bgParseDate(row && row.cDate) || (/^20\d{2}$/.test(yearText) ? new Date(Number(yearText), 0, 1) : null);
|
||||
}
|
||||
|
||||
function bgDateOrYearEnd(row) {
|
||||
var completionYear = bgCompletionYear(row);
|
||||
return bgParseDate(row && row.eDate) || (/^20\d{2}$/.test(completionYear) ? new Date(Number(completionYear), 11, 31) : null);
|
||||
}
|
||||
|
||||
function bgYearCutoff(year) {
|
||||
var targetYear = Number(year || 0);
|
||||
if (!targetYear) return null;
|
||||
var today = bgToday();
|
||||
if (targetYear < today.getFullYear()) return new Date(targetYear, 11, 31);
|
||||
if (targetYear === today.getFullYear()) return today;
|
||||
return null;
|
||||
}
|
||||
|
||||
function bgYearStartDate(year) {
|
||||
var targetYear = Number(year || 0);
|
||||
return targetYear ? new Date(targetYear, 0, 1) : null;
|
||||
}
|
||||
|
||||
function bgActiveInYear(row, year) {
|
||||
var cutoff = bgYearCutoff(year);
|
||||
var yearStart = bgYearStartDate(year);
|
||||
var startDate = bgDateOrYearStart(row);
|
||||
var endDate = bgDateOrYearEnd(row);
|
||||
if (!(cutoff && yearStart && startDate)) return false;
|
||||
if (startDate > cutoff) return false;
|
||||
if (endDate && endDate < yearStart) return false;
|
||||
return !(endDate && endDate <= cutoff);
|
||||
}
|
||||
|
||||
function bgStartedInYear(row, year) {
|
||||
var cutoff = bgYearCutoff(year);
|
||||
var startDate = bgDateOrYearStart(row);
|
||||
if (!(cutoff && startDate)) return false;
|
||||
return startDate.getFullYear() === Number(year || 0) && startDate <= cutoff;
|
||||
}
|
||||
|
||||
function bgCompletedInYear(row, year) {
|
||||
var cutoff = bgYearCutoff(year);
|
||||
var endDate = bgDateOrYearEnd(row);
|
||||
if (!(cutoff && endDate)) return false;
|
||||
return endDate.getFullYear() === Number(year || 0) && endDate <= cutoff;
|
||||
}
|
||||
|
||||
function bgYearRange(row) {
|
||||
var years = [];
|
||||
var startYear = Number(bgDisplayYear(row) || 0);
|
||||
var endYear = Number(bgCompletionYear(row) || 0);
|
||||
if (startYear && endYear && endYear >= startYear) {
|
||||
for (var year = startYear; year <= endYear; year += 1) years.push(String(year));
|
||||
} else if (startYear) {
|
||||
years.push(String(startYear));
|
||||
}
|
||||
return years;
|
||||
}
|
||||
|
||||
function bgYears(rows) {
|
||||
var currentYear = new Date().getFullYear();
|
||||
var years = Array.from(new Set((Array.isArray(rows) ? rows : []).flatMap(bgYearRange).filter(function (year) {
|
||||
return /^20\d{2}$/.test(year);
|
||||
}))).sort(function (a, b) {
|
||||
return Number(b) - Number(a);
|
||||
});
|
||||
years = years.filter(function (year) {
|
||||
var numericYear = Number(year);
|
||||
return numericYear >= 2018 && numericYear <= currentYear;
|
||||
});
|
||||
return years.length ? years : [String(currentYear)];
|
||||
}
|
||||
|
||||
function bgEnsureYear(rows) {
|
||||
var years = bgYears(rows);
|
||||
if (!years.includes(S.dashboard.year)) S.dashboard.year = years[0];
|
||||
return years;
|
||||
}
|
||||
|
||||
function bgTotals(targetRows) {
|
||||
return (Array.isArray(targetRows) ? targetRows : []).reduce(function (acc, row) {
|
||||
acc.c += Number((row && row.cSup) || 0);
|
||||
acc.col += Number((row && row.col) || 0);
|
||||
acc.recv += Number((row && row.recv) || 0);
|
||||
return acc;
|
||||
}, { c: 0, col: 0, recv: 0 });
|
||||
}
|
||||
|
||||
function isSupportServiceRow(row) {
|
||||
var category = String((row && row.cat) || "").trim();
|
||||
return category.indexOf("경영지원") >= 0 || category.indexOf("서비스") >= 0;
|
||||
}
|
||||
|
||||
function isBaronProjectRow(row) {
|
||||
var category = String((row && row.cat) || "").trim();
|
||||
if (category.indexOf("바론") < 0) return false;
|
||||
if (isSupportServiceRow(row)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function bgSummarize(rows, selectedYear) {
|
||||
var items = Array.isArray(rows) ? rows : [];
|
||||
var targetYear = selectedYear || bgEnsureYear(items)[0];
|
||||
var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); });
|
||||
var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); });
|
||||
var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); });
|
||||
var managementRows = newProjectRows.filter(isSupportServiceRow);
|
||||
return {
|
||||
targetYear: targetYear,
|
||||
activeRows: activeRows,
|
||||
newProjectRows: newProjectRows,
|
||||
completedRows: completedRows,
|
||||
managementRows: managementRows,
|
||||
managementTotals: bgTotals(managementRows)
|
||||
};
|
||||
}
|
||||
|
||||
function bgMatches(row) {
|
||||
var section = S.dashboard.section || "active";
|
||||
var selectedYear = S.dashboard.year || bgEnsureYear(S.all)[0];
|
||||
if (section === "new") return bgStartedInYear(row, selectedYear);
|
||||
if (section === "completed") return bgCompletedInYear(row, selectedYear);
|
||||
return bgActiveInYear(row, selectedYear);
|
||||
}
|
||||
|
||||
function normalizeStatusLabel(status) {
|
||||
var value = String(status || "").trim();
|
||||
if (!value) return "-";
|
||||
if (value.indexOf("진행") >= 0) return "과업 진행중";
|
||||
return value;
|
||||
}
|
||||
|
||||
function formatSplitPercent(split) {
|
||||
var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, ""));
|
||||
if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%";
|
||||
return "분담율 " + numeric.toFixed(2) + "%";
|
||||
}
|
||||
|
||||
function projectYear(row) {
|
||||
var start = String((row && row.sDate) || "").trim();
|
||||
var startMatch = start.match(/(20\d{2})/);
|
||||
if (startMatch) return startMatch[1];
|
||||
var name = String((row && row.name) || "").trim();
|
||||
var nameMatch = name.match(/^(20\d{2})/);
|
||||
if (nameMatch) return nameMatch[1];
|
||||
var end = String((row && row.eDate) || "").trim();
|
||||
var endMatch = end.match(/(20\d{2})/);
|
||||
if (endMatch) return endMatch[1];
|
||||
return "미지정";
|
||||
}
|
||||
|
||||
function groupSortRank(row) {
|
||||
var selectedYear = Number((S.dashboard && S.dashboard.year) || projectYear(row) || 0);
|
||||
var startYear = Number(projectYear(row) || 0);
|
||||
if (typeof bgCompletedInYear === "function" && bgCompletedInYear(row, String(selectedYear))) return 9999;
|
||||
if (!startYear) return 9998;
|
||||
return startYear;
|
||||
}
|
||||
|
||||
function tableGroupLabel(row) {
|
||||
var startYear = projectYear(row);
|
||||
if (/^20\d{2}$/.test(startYear)) return startYear + "년";
|
||||
return "미지정";
|
||||
}
|
||||
|
||||
function renderLedgerTable() {
|
||||
var table = document.querySelector(".panel table");
|
||||
if (!table || !E.tbody) return;
|
||||
var thead = table.querySelector("thead");
|
||||
if (thead) {
|
||||
thead.innerHTML = '<tr>'
|
||||
+ '<th><div class="th-head"><button type="button" class="th-trigger" data-filter="cat" data-label="구분"><span class="th-title">구분</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterCatMenu" class="th-menu" data-filter="cat"></div></div></th>'
|
||||
+ '<th><div class="th-head"><button type="button" class="th-trigger" data-filter="code" data-label="사업코드"><span class="th-title">사업코드</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterCodeMenu" class="th-menu" data-filter="code"></div></div></th>'
|
||||
+ '<th><div class="th-head"><button type="button" class="th-trigger" data-filter="name" data-label="사업명(계약명)"><span class="th-title">사업명(계약명)</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterNameMenu" class="th-menu" data-filter="name"></div></div></th>'
|
||||
+ '<th><div class="th-head"><button type="button" class="th-trigger" data-filter="client" data-label="발주처(계약처)"><span class="th-title">발주처(계약처)</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterClientMenu" class="th-menu" data-filter="client"></div></div></th>'
|
||||
+ '<th><div class="th-head"><button type="button" class="th-trigger" data-filter="order" data-label="발주방법"><span class="th-title">발주방법</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterOrderMenu" class="th-menu" data-filter="order"></div></div></th>'
|
||||
+ '<th><div class="th-head"><button type="button" class="th-trigger" data-filter="status" data-label="진행상태"><span class="th-title">진행상태</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterStatusMenu" class="th-menu" data-filter="status"></div></div></th>'
|
||||
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="amount" data-label="계약금"><span class="th-title">계약금</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterAmountMenu" class="th-menu" data-filter="amount"></div></div></th>'
|
||||
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="outsource" data-label="외주비"><span class="th-title">외주비</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterOutsourceMenu" class="th-menu" data-filter="outsource"></div></div></th>'
|
||||
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="receivable" data-label="미수금"><span class="th-title">미수금</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterReceivableMenu" class="th-menu" data-filter="receivable"></div></div></th>'
|
||||
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="collected" data-label="수금액"><span class="th-title">수금액</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterCollectedMenu" class="th-menu" data-filter="collected"></div></div></th>'
|
||||
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="rate" data-label="수금률"><span class="th-title">수금률</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterRateMenu" class="th-menu" data-filter="rate"></div></div></th>'
|
||||
+ "</tr>";
|
||||
}
|
||||
var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(function (a, b) {
|
||||
var ar = groupSortRank(a);
|
||||
var br = groupSortRank(b);
|
||||
if (ar !== br) return ar - br;
|
||||
return Number(b.recv || 0) - Number(a.recv || 0);
|
||||
});
|
||||
S.viewRows = rows;
|
||||
var lastGroupLabel = "";
|
||||
E.tbody.innerHTML = rows.map(function (r) {
|
||||
var groupLabel = tableGroupLabel(r);
|
||||
var isCollapsed = !!S.collapsedGroups[groupLabel];
|
||||
var groupRow = "";
|
||||
if (groupLabel !== lastGroupLabel) {
|
||||
groupRow = '<tr class="group-row"><td colspan="11"><button type="button" class="group-chip" data-group-label="' + escAttr(groupLabel) + '"><span>' + esc(groupLabel) + '</span><span class="group-toggle" aria-hidden="true">' + (isCollapsed ? "+" : "-") + "</span></button></td></tr>";
|
||||
lastGroupLabel = groupLabel;
|
||||
}
|
||||
if (isCollapsed) return groupRow;
|
||||
return groupRow + '<tr class="' + (isSettledRow(r) ? 'settled' : '') + '">'
|
||||
+ '<td><div class="badge ' + esc(String(r.cat || "").indexOf("바론") >= 0 ? 'badge-baron' : 'badge-family') + '">' + esc(r.cat || "-") + '</div></td>'
|
||||
+ '<td><div class="subline" style="margin-top:0;font-size:12px;color:#66756d">' + esc(r.code || "-") + '</div></td>'
|
||||
+ '<td><button type="button" class="project-link" data-project-key="' + escAttr(String(r.code || "") + "|" + String(r.name || "")) + '">' + esc(r.name || "-") + '</button><div class="subline">' + esc(r.periodText || "-") + '</div></td>'
|
||||
+ '<td><div class="client-main">' + esc((r.client || "").trim() || "-") + '</div><div class="subline">' + esc(formatSplitPercent(r.split)) + '</div></td>'
|
||||
+ '<td><div>' + esc(r.order || "-") + '</div></td>'
|
||||
+ '<td><div class="badge ' + (String(r.status || "").indexOf("완료") >= 0 ? 'ok' : '') + '">' + esc(normalizeStatusLabel(r.status)) + '</div></td>'
|
||||
+ '<td class="num"><strong>' + esc(won(r.cSup || 0)) + '</strong></td>'
|
||||
+ '<td class="num"><strong>' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '</strong></td>'
|
||||
+ '<td class="num"><strong>' + esc(won(r.recv || 0)) + '</strong></td>'
|
||||
+ '<td class="num"><strong>' + esc(won(r.col || 0)) + '</strong></td>'
|
||||
+ '<td class="num"><strong style="color:' + (isSettledRow(r) ? '#b7aa93' : '#1a5645') + '">' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '</strong></td>'
|
||||
+ '</tr>';
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function renderCollectionBoard(r) {
|
||||
var payments = Array.isArray(r.payments) && r.payments.length ? r.payments : [{
|
||||
pay: r.pay || "-",
|
||||
issueDate: r.issueDate || "",
|
||||
collectDate: r.collectDateSummary || r.colDate || "",
|
||||
collected: r.col || 0,
|
||||
receivable: r.recv || Math.max(0, Number(r.sTot || 0) - Number(r.col || 0)),
|
||||
note: r.note || "",
|
||||
status: r.status || ""
|
||||
}];
|
||||
return '<div class="ledger-block collect"><div class="ledger-head"><div class="ledger-head-left"><div class="ledger-icon">C</div><div><div class="ledger-name">수금 및 기성 현황</div><div class="ledger-sub">기성 차수별 세금계산서 발행 및 수금 내역</div></div></div><div class="ledger-pill">총 수금 ' + esc(won(r.col || 0)) + '</div></div><div class="ledger-table-wrap"><table class="ledger-table"><thead><tr><th>기성 차수</th><th>세금계산서 발행일</th><th>수금일</th><th style="text-align:right">수금금액</th><th style="text-align:right">미수금액</th><th>비고</th></tr></thead><tbody>'
|
||||
+ payments.map(function (payment, index) {
|
||||
var noteParts = [];
|
||||
if (payment.status) noteParts.push(payment.status);
|
||||
if (payment.note) noteParts.push(payment.note);
|
||||
return '<tr><td><span class="ledger-main">' + esc((index + 1) + "차") + '</span><span class="ledger-muted">' + esc(payment.pay || "-") + '</span></td><td><span class="ledger-main">' + esc(payment.issueDate ? d(payment.issueDate) : "-") + '</span></td><td><span class="ledger-main">' + esc(payment.collectDate ? d(payment.collectDate) : "-") + '</span></td><td class="ledger-amount">' + esc(won(payment.collected || 0)) + '</td><td class="ledger-amount" style="color:#a94832">' + esc(won(payment.receivable || 0)) + '</td><td><span class="ledger-note">' + esc(noteParts.join(" / ") || "-") + '</span></td></tr>';
|
||||
}).join("")
|
||||
+ "</tbody></table></div></div>";
|
||||
}
|
||||
|
||||
function renderContactCard(label, name, company, department, phone, email) {
|
||||
var hasValue = [name, company, department, phone, email].some(function (value) {
|
||||
return String(value || "").trim() !== "";
|
||||
});
|
||||
if (!hasValue) {
|
||||
return '<div class="inline-card"><div class="kvk">' + esc(label) + '</div><div class="summary-note">등록된 담당자 정보가 없습니다.</div></div>';
|
||||
}
|
||||
return '<div class="inline-card"><div class="kvk">' + esc(label) + '</div><div class="project-meta-grid">'
|
||||
+ '<div class="kv"><div class="kvk">이름</div><div class="kvv">' + esc(name || "-") + '</div></div>'
|
||||
+ '<div class="kv"><div class="kvk">소속</div><div class="kvv">' + esc(company || "-") + '</div><div class="summary-note">' + esc(department || "-") + '</div></div>'
|
||||
+ '<div class="kv"><div class="kvk">연락처</div><div class="kvv">' + esc(phone || "-") + '</div></div>'
|
||||
+ '<div class="kv"><div class="kvk">이메일</div><div class="kvv">' + esc(email || "-") + '</div></div>'
|
||||
+ "</div></div>";
|
||||
}
|
||||
|
||||
function renderProjectInline(r) {
|
||||
var payments = Array.isArray(r.payments) ? r.payments : [];
|
||||
var latestCollect = d(r.collectDateSummary || r.colDate);
|
||||
var hasOutsource = (Array.isArray(r.outsourceItems) && r.outsourceItems.length > 0) || Number(r.outsourceCost || 0) > 0 || Number(r.outsourcePaid || 0) > 0 || Number(r.outsourceRemaining || 0) > 0;
|
||||
var clientDisplay = typeof normalizeClientDisplay === "function" ? normalizeClientDisplay(r.client) : (String(r.client || "").trim() || "-");
|
||||
var splitDisplay = typeof formatSplitDisplay === "function" ? formatSplitDisplay(r.split) : formatSplitPercent(r.split).replace("분담율 ", "");
|
||||
var summaryCards = [
|
||||
'<div class="summary-card"><div class="summary-label">계약금</div><div class="summary-value">' + esc(won(r.cSup || 0)) + '</div><div class="summary-note"></div></div>',
|
||||
'<div class="summary-card"><div class="summary-label">수금액</div><div class="summary-value">' + esc(won(r.col || 0)) + '</div><div class="summary-note">' + esc(latestCollect === "-" ? "수금일 없음" : "최종 수금일 " + latestCollect) + '</div></div>',
|
||||
'<div class="summary-card"><div class="summary-label">수금률</div><div class="summary-value">' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '</div><div class="summary-note">' + esc(payments.length ? "기성 " + payments.length + "차까지 반영" : "차수 정보 없음") + '</div></div>',
|
||||
'<div class="summary-card receivable"><div class="summary-label">미수금액</div><div class="summary-value">' + esc(won(r.recv || 0)) + '</div><div class="summary-note">잔여 수금 필요 금액</div></div>'
|
||||
].join("");
|
||||
var boards = [
|
||||
hasOutsource && typeof renderOutsourceBoard === "function" ? renderOutsourceBoard(r) : "",
|
||||
renderCollectionBoard(r)
|
||||
].filter(Boolean).join("");
|
||||
return '<div class="inline-panel"><div class="project-head project-head-grid"><div class="project-head-main"><div class="inline-card"><div class="project-meta-grid"><div class="kv"><div class="kvk">계약법인</div><div class="kvv">' + esc(r.corp || "-") + '</div></div><div class="kv"><div class="kvk">발주처</div><div class="kvv">' + esc(clientDisplay) + '</div><div class="summary-note">' + esc(splitDisplay ? "분담율 " + splitDisplay : "분담율 -") + '</div></div><div class="kv"><div class="kvk">발주방법</div><div class="kvv">' + esc(r.order || "-") + '</div></div><div class="kv"><div class="kvk">PM</div><div class="kvv">' + esc(r.pm || "-") + '</div></div></div></div><div class="inline-card"><div class="summary-grid">' + summaryCards + '</div><div class="project-progress progress"><div class="bar" style="width:' + esc(String(Math.max(0, Math.min(100, Number(r.rate || 0))))) + '%"></div></div></div></div><div class="project-contact-stack">' + renderContactCard("계약 / 청구 담당자", r.cmNm, r.cmCo, r.cmDp, r.cmPh, r.cmEm) + renderContactCard("부서 담당자", r.dmNm, r.dmCo, r.dmDp, r.dmPh, r.dmEm) + '</div></div><div class="ledger-stack">' + boards + '</div></div>';
|
||||
}
|
||||
|
||||
function openProjectWindow(r) {
|
||||
var popupKey = typeof rowKey === "function"
|
||||
? rowKey(r).replace(/[^0-9a-zA-Z]/g, "_")
|
||||
: String((r.code || "project") + "_" + (r.name || "")).replace(/[^0-9a-zA-Z_]/g, "_");
|
||||
var popup = window.open("", "business_project_" + popupKey, "width=1600,height=980,resizable=yes,scrollbars=yes");
|
||||
if (!popup) return;
|
||||
var styleText = Array.from(document.querySelectorAll("style")).map(function (el) {
|
||||
return el.textContent || "";
|
||||
}).join("\n");
|
||||
var detailHtml = renderProjectInline(r);
|
||||
var pageHtml = '<!DOCTYPE html><html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>'
|
||||
+ esc(r.name || "사업 상세")
|
||||
+ '</title><link rel="stylesheet" href="/design-tokens.css?v=20260401-01"><link rel="stylesheet" href="/design-patterns.css?v=20260401-01"><style>' + styleText
|
||||
+ 'body{margin:0;background:#f1eadf;color:#10251d;font-family:"Pretendard","Noto Sans KR","Malgun Gothic",sans-serif;}'
|
||||
+ '.popup-wrap{max-width:1680px;margin:0 auto;padding:20px;}'
|
||||
+ '@media (max-width: 1180px){.project-head-grid{grid-template-columns:1fr;}.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.project-meta-grid{grid-template-columns:1fr;}}'
|
||||
+ '@media (max-width: 760px){.popup-wrap{padding:14px;}.summary-grid{grid-template-columns:1fr;}.ledger-head{flex-direction:column;align-items:flex-start;}.ledger-pill{white-space:normal;}.ledger-table-wrap{padding:0 10px 12px;overflow-x:auto;}}'
|
||||
+ '</style></head><body><div class="popup-wrap"><div class="popup-head"><div class="popup-title">' + esc(r.name || "-") + '</div><div class="popup-sub">사업코드 ' + esc(r.code || "-") + ' · 계약법인 ' + esc(r.corp || "-") + '</div></div>' + detailHtml + "</div></body></html>";
|
||||
popup.document.open();
|
||||
popup.document.write(pageHtml);
|
||||
popup.document.close();
|
||||
popup.focus();
|
||||
}
|
||||
|
||||
async function tryLoadDbDefaultBusinessLedger() {
|
||||
if (window.__mhBusinessDefaultLoaded) return;
|
||||
window.__mhBusinessDefaultLoaded = true;
|
||||
try {
|
||||
var response = await fetch("/api/integration/business-ledger-default");
|
||||
if (!response.ok) throw new Error("기본 사업관리대장 원본을 불러오지 못했습니다.");
|
||||
var fileName = response.headers.get("x-source-filename") || "사업관리대장-1.xlsx";
|
||||
var buffer = await response.arrayBuffer();
|
||||
if (!buffer || !buffer.byteLength) throw new Error("기본 사업관리대장 원본 데이터가 비어 있습니다.");
|
||||
await loadLedgerFile(buffer, fileName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function applyDashboardChrome() {
|
||||
if (!E.cards) return;
|
||||
document.body.setAttribute("data-mh-ledger-enhanced", "true");
|
||||
var wrap = document.querySelector(".wrap");
|
||||
var panel = document.querySelector(".panel");
|
||||
if (wrap && panel) {
|
||||
var shell = wrap.querySelector(".business-shell");
|
||||
if (!shell) {
|
||||
shell = document.createElement("div");
|
||||
shell.className = "business-shell";
|
||||
wrap.insertBefore(shell, E.cards);
|
||||
}
|
||||
if (E.cards.parentNode !== shell) shell.appendChild(E.cards);
|
||||
if (panel.parentNode !== shell) shell.appendChild(panel);
|
||||
}
|
||||
var years = bgEnsureYear(S.all);
|
||||
var summary = bgSummarize(S.all, S.dashboard.year);
|
||||
var rows = Array.isArray(S.rows) ? S.rows : [];
|
||||
var visibleBaronProjectRows = rows.filter(isBaronProjectRow);
|
||||
var totals = bgTotals(visibleBaronProjectRows);
|
||||
var totalRate = typeof rate === "function" ? rate("", totals.col, totals.col + totals.recv) : 0;
|
||||
var toolbarHtml = '<div class="cards-toolbar">'
|
||||
+ '<div class="cards-toolbar-row">'
|
||||
+ years.map(function (year) {
|
||||
return '<button type="button" class="summary-year-chip ' + (S.dashboard.year === year ? "active" : "") + '" data-dashboard-year="' + escAttr(year) + '">' + esc(year) + "</button>";
|
||||
}).join("")
|
||||
+ '<div class="cards-toolbar-search"></div>'
|
||||
+ "</div>"
|
||||
+ '<div class="cards-toolbar-metrics">'
|
||||
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "active" ? "active" : "") + '" data-dashboard-section="active"><span class="label">' + esc(summary.targetYear) + '년 진행과업</span><span class="count">' + summary.activeRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">전년도 이월 사업 포함</span></button>'
|
||||
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "new" ? "active" : "") + '" data-dashboard-section="new"><span class="label">' + esc(summary.targetYear) + '년 신규프로젝트</span><span class="count">' + summary.newProjectRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">계약기간 시작년도 기준</span></button>'
|
||||
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "completed" ? "active" : "") + '" data-dashboard-section="completed"><span class="label">' + esc(summary.targetYear) + '년 완료과업</span><span class="count">' + summary.completedRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">해당년도 종료 사업 기준</span></button>'
|
||||
+ "</div></div>";
|
||||
var cards = [
|
||||
{ label: summary.targetYear + "년 프로젝트", value: visibleBaronProjectRows.length.toLocaleString("ko-KR") + " 건", note: "" },
|
||||
{ label: "계약금", value: won(totals.c), note: "" },
|
||||
{ label: "수금액", value: won(totals.col), note: "" },
|
||||
{ label: "미수금", value: won(totals.recv), note: "" },
|
||||
{ label: "수금률(%)", value: totalRate.toFixed(2) + "%", note: "" },
|
||||
{ label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" }
|
||||
];
|
||||
E.cards.innerHTML = toolbarHtml + cards.map(function (card) {
|
||||
return '<div class="card ' + esc(card.className || "") + '"><div class="k">' + esc(card.label) + '</div><div class="v">' + esc(card.value) + '</div><div class="n">' + esc(card.note || "") + "</div></div>";
|
||||
}).join("");
|
||||
var searchWrap = E.cards.querySelector(".cards-toolbar-search");
|
||||
if (searchWrap && E.search) {
|
||||
searchWrap.appendChild(E.search);
|
||||
E.search.placeholder = "전체 검색";
|
||||
}
|
||||
}
|
||||
|
||||
var originalRender = render;
|
||||
render = function () {
|
||||
originalRender();
|
||||
applyDashboardChrome();
|
||||
renderLedgerTable();
|
||||
};
|
||||
|
||||
filter = function () {
|
||||
bgEnsureYear(S.all);
|
||||
var q = String(E.search.value || "").trim().toLowerCase();
|
||||
var searched = !q ? S.all.slice() : S.all.filter(function (r) {
|
||||
return [r.code, r.name, r.client, r.pm, r.status, r.cat, r.corp, r.pay, (r.payments || []).map(function (p) { return p.pay; }).join(" "), r.periodText].join(" ").toLowerCase().includes(q);
|
||||
});
|
||||
S.rows = searched.filter(function (r) {
|
||||
return bgMatches(r) && matchesColumnFilters(r);
|
||||
});
|
||||
render();
|
||||
};
|
||||
|
||||
if (E.cards && !E.cards.dataset.dashboardBound) {
|
||||
E.cards.dataset.dashboardBound = "true";
|
||||
E.cards.addEventListener("click", function (event) {
|
||||
var yearButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-year]") : null;
|
||||
if (yearButton) {
|
||||
S.dashboard.year = yearButton.getAttribute("data-dashboard-year") || S.dashboard.year;
|
||||
filter();
|
||||
return;
|
||||
}
|
||||
var sectionButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-section]") : null;
|
||||
if (sectionButton) {
|
||||
S.dashboard.section = sectionButton.getAttribute("data-dashboard-section") || "active";
|
||||
filter();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (E.tbody && !E.tbody.dataset.projectBound) {
|
||||
E.tbody.dataset.projectBound = "true";
|
||||
E.tbody.addEventListener("click", function (event) {
|
||||
var groupButton = event.target && event.target.closest ? event.target.closest("[data-group-label]") : null;
|
||||
if (groupButton) {
|
||||
var label = groupButton.getAttribute("data-group-label") || "";
|
||||
if (label) {
|
||||
S.collapsedGroups[label] = !S.collapsedGroups[label];
|
||||
render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
var trigger = event.target && event.target.closest ? event.target.closest(".project-link") : null;
|
||||
if (!trigger) return;
|
||||
var key = trigger.getAttribute("data-project-key") || "";
|
||||
var rows = Array.isArray(S.viewRows) ? S.viewRows : [];
|
||||
var row = rows.find(function (item) {
|
||||
return (String(item.code || "") + "|" + String(item.name || "")) === key;
|
||||
});
|
||||
if (row) openProjectWindow(row);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
try {
|
||||
filter();
|
||||
if (typeof loadLedgerFile === "function") {
|
||||
tryLoadDbDefaultBusinessLedger();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
window.addEventListener("message", function (event) {
|
||||
var data = event.data || {};
|
||||
if (data.source !== "total-upload" || data.type !== "business") return;
|
||||
setTimeout(function () {
|
||||
try {
|
||||
applyDashboardChrome();
|
||||
renderLedgerTable();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
})();
|
||||
BIN
incoming-files/사업관리대장/사업관리대장-1.xlsx
Normal file
BIN
incoming-files/사업관리대장/사업관리대장-1.xlsx
Normal file
Binary file not shown.
1377
incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.css
Normal file
1377
incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.css
Normal file
File diff suppressed because it is too large
Load Diff
2598
incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.html
Normal file
2598
incoming-files/사업관리대장/사업관리대장/MH 통합 대시보드_260320.html
Normal file
File diff suppressed because one or more lines are too long
BIN
incoming-files/사업관리대장/사업관리대장/사업관리대장-1.xlsx
Normal file
BIN
incoming-files/사업관리대장/사업관리대장/사업관리대장-1.xlsx
Normal file
Binary file not shown.
@@ -1,38 +1,41 @@
|
||||
@import url("/design-tokens.css?v=20260401-01");
|
||||
@import url("/design-patterns.css?v=20260401-01");
|
||||
|
||||
:root {
|
||||
--font-sans: "Pretendard", sans-serif;
|
||||
--font-sans: var(--ds-font-sans);
|
||||
|
||||
--color-bg: #f1f5f9;
|
||||
--color-bg-soft: #eef2ff;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-soft: rgba(255, 255, 255, 0.88);
|
||||
--color-surface-strong: #e2e8f0;
|
||||
--color-text: #1e293b;
|
||||
--color-text-soft: #475569;
|
||||
--color-text-muted: #64748b;
|
||||
--color-border: #cbd5e1;
|
||||
--color-border-soft: rgba(148, 163, 184, 0.3);
|
||||
--color-header: #1e293b;
|
||||
--color-header-soft: #334155;
|
||||
--color-accent: #4f46e5;
|
||||
--color-accent-soft: #e0e7ff;
|
||||
--color-accent-strong: #4338ca;
|
||||
--color-bg: var(--ds-bg);
|
||||
--color-bg-soft: var(--ds-bg-soft);
|
||||
--color-surface: var(--ds-panel);
|
||||
--color-surface-soft: var(--ds-panel-soft);
|
||||
--color-surface-strong: var(--ds-panel-strong);
|
||||
--color-text: var(--ds-ink);
|
||||
--color-text-soft: var(--ds-text-soft);
|
||||
--color-text-muted: var(--ds-text-muted);
|
||||
--color-border: var(--ds-line);
|
||||
--color-border-soft: var(--ds-line-soft);
|
||||
--color-header: var(--ds-brand);
|
||||
--color-header-soft: var(--ds-brand-soft);
|
||||
--color-accent: var(--ds-accent);
|
||||
--color-accent-soft: var(--ds-accent-soft);
|
||||
--color-accent-strong: var(--ds-accent-strong);
|
||||
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 18px;
|
||||
--radius-xl: 24px;
|
||||
--radius-pill: 999px;
|
||||
--radius-sm: var(--ds-radius-sm);
|
||||
--radius-md: var(--ds-radius-md);
|
||||
--radius-lg: var(--ds-radius-lg);
|
||||
--radius-xl: var(--ds-radius-xl);
|
||||
--radius-pill: var(--ds-radius-pill);
|
||||
|
||||
--shadow-soft: 0 4px 14px rgba(15, 23, 42, 0.08);
|
||||
--shadow-card: 0 18px 44px rgba(15, 23, 42, 0.12);
|
||||
--shadow-float: 0 18px 36px rgba(79, 70, 229, 0.16);
|
||||
--shadow-soft: var(--ds-shadow-soft);
|
||||
--shadow-card: var(--ds-shadow-card);
|
||||
--shadow-float: var(--ds-shadow-float);
|
||||
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-1: var(--ds-space-1);
|
||||
--space-2: var(--ds-space-2);
|
||||
--space-3: var(--ds-space-3);
|
||||
--space-4: var(--ds-space-4);
|
||||
--space-5: var(--ds-space-5);
|
||||
--space-6: var(--ds-space-6);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -42,17 +45,17 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(79, 70, 229, 0.12), transparent 22%),
|
||||
radial-gradient(circle at bottom right, rgba(148, 163, 184, 0.18), transparent 28%),
|
||||
var(--color-bg);
|
||||
background: var(--ds-bg-gradient);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--ds-bg-gradient);
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -90,18 +93,18 @@ a {
|
||||
.ui-button-secondary {
|
||||
border: 1px solid var(--color-border-soft);
|
||||
color: var(--color-text);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
background: var(--ds-surface-tint);
|
||||
}
|
||||
|
||||
.ui-input {
|
||||
border: 1px solid var(--color-border-soft);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
background: var(--ds-surface-tint-strong);
|
||||
color: var(--color-text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ui-input:focus {
|
||||
border-color: rgba(79, 70, 229, 0.45);
|
||||
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.08);
|
||||
border-color: rgba(47, 153, 115, 0.45);
|
||||
box-shadow: 0 0 0 4px rgba(47, 153, 115, 0.1);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,17 @@ let isListMode = false;
|
||||
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
||||
let photoPreviewObjectUrl = null;
|
||||
let seatMapLayoutCache = null;
|
||||
let activeAsOfDate = '';
|
||||
let isHistoricalSnapshot = false;
|
||||
const listViewState = {
|
||||
mode: 'current',
|
||||
snapshotDate: '',
|
||||
compareFromDate: '',
|
||||
compareToDate: '',
|
||||
snapshotMembers: [],
|
||||
compareItems: [],
|
||||
};
|
||||
const seatMapOfficeKeys = ['technical-development-center', 'hanmac-building-6f', 'hanmac-building-7f'];
|
||||
|
||||
const levelOrder = ['부서', '그룹', '디비전', '팀', '셀'];
|
||||
const dropdownFields = ['소속회사', '직급', '직책', ...levelOrder];
|
||||
@@ -33,6 +44,15 @@ function cloneMembers(items) {
|
||||
return JSON.parse(JSON.stringify(items));
|
||||
}
|
||||
|
||||
function isRetiredLegacyMember(member) {
|
||||
const workStatus = String(member?.['근무상태'] || '').trim();
|
||||
return workStatus === '퇴직';
|
||||
}
|
||||
|
||||
function getVisibleLegacyMembers(items) {
|
||||
return (items || []).filter((member) => !isRetiredLegacyMember(member));
|
||||
}
|
||||
|
||||
function getPhotoPlaceholder(name = '') {
|
||||
return `https://via.placeholder.com/160?text=${encodeURIComponent(name || 'Profile')}`;
|
||||
}
|
||||
@@ -116,6 +136,22 @@ async function apiFetch(url, options = {}) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
function withAsOf(url) {
|
||||
if (!activeAsOfDate) {
|
||||
return url;
|
||||
}
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}as_of=${encodeURIComponent(activeAsOfDate)}`;
|
||||
}
|
||||
|
||||
function getDefaultHistoryDate() {
|
||||
if (activeAsOfDate) {
|
||||
return activeAsOfDate;
|
||||
}
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||||
}
|
||||
|
||||
async function uploadProfilePhoto(file, memberName) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
@@ -128,7 +164,7 @@ async function uploadProfilePhoto(file, memberName) {
|
||||
}
|
||||
|
||||
function setMembers(items) {
|
||||
members = items.map(toLegacyMember);
|
||||
members = getVisibleLegacyMembers(items.map(toLegacyMember));
|
||||
if (selectedDept !== '전체' && !members.some((member) => member['부서'] === selectedDept)) {
|
||||
selectedDept = '전체';
|
||||
}
|
||||
@@ -139,7 +175,7 @@ async function loadMembers(message) {
|
||||
if (message) {
|
||||
emptyStateMessage = message;
|
||||
}
|
||||
const payload = await apiFetch('/api/members');
|
||||
const payload = await apiFetch(withAsOf('/api/members'));
|
||||
setMembers(payload.items || []);
|
||||
if (!members.length) {
|
||||
emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
||||
@@ -147,42 +183,92 @@ async function loadMembers(message) {
|
||||
render();
|
||||
}
|
||||
|
||||
async function loadActiveSeatMapLayout(force = false) {
|
||||
async function loadSeatMapLayouts(force = false) {
|
||||
if (seatMapLayoutCache && !force) {
|
||||
return seatMapLayoutCache;
|
||||
}
|
||||
try {
|
||||
const activePayload = await apiFetch('/api/seat-maps/active');
|
||||
const seatMap = activePayload?.item;
|
||||
if (!seatMap?.id) {
|
||||
seatMapLayoutCache = null;
|
||||
return null;
|
||||
}
|
||||
const layoutPayload = await apiFetch(`/api/seat-maps/${seatMap.id}/layout`);
|
||||
seatMapLayoutCache = layoutPayload;
|
||||
return layoutPayload;
|
||||
const layouts = (await Promise.all(seatMapOfficeKeys.map(async (officeKey) => {
|
||||
try {
|
||||
const activePayload = await apiFetch(`/api/seat-maps/active?office_key=${encodeURIComponent(officeKey)}`);
|
||||
const seatMap = activePayload?.item;
|
||||
if (!seatMap?.id) {
|
||||
return null;
|
||||
}
|
||||
return await apiFetch(withAsOf(`/api/seat-maps/${seatMap.id}/layout`));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}))).filter(Boolean);
|
||||
seatMapLayoutCache = layouts;
|
||||
return layouts;
|
||||
} catch {
|
||||
seatMapLayoutCache = null;
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getMemberSeatInfo(layout, memberId) {
|
||||
if (!layout || !memberId) {
|
||||
function handleSeatMapLayoutUpdated() {
|
||||
seatMapLayoutCache = null;
|
||||
loadMembers().catch(() => { });
|
||||
}
|
||||
|
||||
function getMemberSeatInfo(layouts, memberId) {
|
||||
if (!Array.isArray(layouts) || !memberId) {
|
||||
return null;
|
||||
}
|
||||
const placement = (layout.placements || []).find((item) => Number(item.member_id) === Number(memberId));
|
||||
if (!placement) {
|
||||
return null;
|
||||
for (const layout of layouts) {
|
||||
const placement = (layout.placements || []).find((item) => Number(item.member_id) === Number(memberId));
|
||||
if (!placement) {
|
||||
continue;
|
||||
}
|
||||
const slot = (layout.slots || []).find((item) => Number(item.id) === Number(placement.seat_slot_id));
|
||||
return {
|
||||
layout,
|
||||
seatMapId: layout.seat_map?.id || null,
|
||||
seatMapName: layout.seat_map?.name || '자리배치도',
|
||||
seatLabel: placement.seat_label || slot?.label || '',
|
||||
slotKey: slot?.slot_key || '',
|
||||
assigned: true,
|
||||
};
|
||||
}
|
||||
const slot = (layout.slots || []).find((item) => Number(item.id) === Number(placement.seat_slot_id));
|
||||
return {
|
||||
seatMapId: layout.seat_map?.id || null,
|
||||
seatMapName: layout.seat_map?.name || '자리배치도',
|
||||
seatLabel: placement.seat_label || slot?.label || '',
|
||||
slotKey: slot?.slot_key || '',
|
||||
assigned: true,
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildSeatAssignments(layout) {
|
||||
if (!layout || !Array.isArray(layout.placements) || !Array.isArray(layout.members) || !Array.isArray(layout.slots)) {
|
||||
return [];
|
||||
}
|
||||
return layout.placements.map((placement) => {
|
||||
const slot = layout.slots.find((item) => Number(item.id) === Number(placement.seat_slot_id));
|
||||
const memberItem = layout.members.find((item) => Number(item.id) === Number(placement.member_id));
|
||||
if (!slot || !memberItem) return null;
|
||||
return {
|
||||
key: String(slot.slot_key || ''),
|
||||
member_id: Number(memberItem.id),
|
||||
name: memberItem.name || '-',
|
||||
rank: memberItem.rank || '-',
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function applySeatPreviewFrameState(frame, seatInfo, layout) {
|
||||
if (!frame?.contentWindow || !seatInfo?.slotKey) {
|
||||
return;
|
||||
}
|
||||
const postState = () => {
|
||||
if (!frame.contentWindow) {
|
||||
return;
|
||||
}
|
||||
frame.contentWindow.postMessage({
|
||||
type: 'seatmap-set-assignments',
|
||||
items: buildSeatAssignments(layout),
|
||||
}, window.location.origin);
|
||||
frame.contentWindow.postMessage({ type: 'seatmap-set-mode', mode: 'compact' }, window.location.origin);
|
||||
frame.contentWindow.postMessage({ type: 'seatmap-focus-chair', key: seatInfo.slotKey, padding: 2600 }, window.location.origin);
|
||||
};
|
||||
postState();
|
||||
setTimeout(postState, 120);
|
||||
}
|
||||
|
||||
async function syncMembers(nextMembers) {
|
||||
@@ -567,6 +653,10 @@ function render() {
|
||||
}
|
||||
|
||||
function toggleAdminMode(checked) {
|
||||
if (checked && isHistoricalSnapshot) {
|
||||
alert('월말 히스토리 조회 중에는 수정할 수 없습니다. 최신 월로 돌아간 뒤 수정해주세요.');
|
||||
return;
|
||||
}
|
||||
isAdmin = checked;
|
||||
const button = document.getElementById('admin-mode-btn');
|
||||
if (isAdmin) {
|
||||
@@ -593,8 +683,8 @@ function updateFabMenu() {
|
||||
const menu = document.getElementById('fab-menu');
|
||||
let html = '<button class="fab-sub shadow-xl" data-label="리스트" onclick="openListViewModal(event)">📋</button>';
|
||||
html += '<button class="fab-sub shadow-xl" data-label="조직도 인쇄(A3)" onclick="printA3()">🖨️</button>';
|
||||
if (isAdmin) {
|
||||
html += '<button class="fab-sub shadow-xl" data-label="자리배치도" onclick="openSeatMapView(event)">🪑</button>';
|
||||
html += '<button class="fab-sub shadow-xl" data-label="자리배치도" onclick="openSeatMapView(event)">🪑</button>';
|
||||
if (isAdmin && !isHistoricalSnapshot) {
|
||||
html += '<button class="fab-sub shadow-xl" data-label="조직현황 업로드" onclick="triggerUpload(event)">⬆️</button>';
|
||||
html += '<button class="fab-sub shadow-xl" data-label="신규 구성원" onclick="openAddModal(event)">👤</button>';
|
||||
html += '<button class="fab-sub shadow-xl" data-label="신규 팀/그룹/셀" onclick="openUnitAddModal(event)">🏢</button>';
|
||||
@@ -602,14 +692,60 @@ function updateFabMenu() {
|
||||
menu.innerHTML = html;
|
||||
}
|
||||
|
||||
async function openHistoryCompareModal(fromDate, toDate) {
|
||||
openListViewModal();
|
||||
const fromInput = document.getElementById('list-compare-from');
|
||||
const toInput = document.getElementById('list-compare-to');
|
||||
if (fromInput) {
|
||||
fromInput.value = fromDate || '';
|
||||
}
|
||||
if (toInput) {
|
||||
toInput.value = toDate || '';
|
||||
}
|
||||
await loadCompareListView();
|
||||
}
|
||||
|
||||
function openSeatMapView(event) {
|
||||
event.stopPropagation();
|
||||
document.getElementById('fab-container').classList.remove('active');
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'open-seatmap' }, '*');
|
||||
window.parent.postMessage({ type: 'open-seatmap', readOnly: !isAdmin }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
return;
|
||||
}
|
||||
if (data.type === 'date-range') {
|
||||
activeAsOfDate = String(data.endDate || '').slice(0, 10);
|
||||
return;
|
||||
}
|
||||
if (data.type === 'organization-history-view') {
|
||||
activeAsOfDate = String(data.asOfDate || '').slice(0, 10);
|
||||
isHistoricalSnapshot = Boolean(data.historical);
|
||||
if (isHistoricalSnapshot && isAdmin) {
|
||||
toggleAdminMode(false);
|
||||
} else {
|
||||
updateFabMenu();
|
||||
render();
|
||||
}
|
||||
seatMapLayoutCache = null;
|
||||
loadMembers().catch(() => { });
|
||||
return;
|
||||
}
|
||||
if (data.type === 'open-history-compare') {
|
||||
openHistoryCompareModal(String(data.fromDate || ''), String(data.toDate || '')).catch((error) => {
|
||||
alert(error.message || '변경 비교를 불러오지 못했습니다.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data.type === 'seatmap-layout-updated') {
|
||||
handleSeatMapLayoutUpdated();
|
||||
}
|
||||
});
|
||||
|
||||
function triggerUpload(event) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
@@ -724,18 +860,20 @@ function openUnitAddModal(event) {
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-[11px] font-black text-slate-600 block">상위 위치 선택</label>
|
||||
<select id="new-unit-parent" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none"></select>
|
||||
<label class="member-form-label block">상위 위치 선택</label>
|
||||
<select id="new-unit-parent" class="member-form-select"></select>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-[11px] font-black text-slate-600 block">신규 명칭 입력</label>
|
||||
<input id="new-unit-name" placeholder="예: 신규개발팀" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
|
||||
<label class="member-form-label block">신규 명칭 입력</label>
|
||||
<input id="new-unit-name" placeholder="예: 신규개발팀" class="member-form-input">
|
||||
</div>
|
||||
`;
|
||||
updateParentList();
|
||||
document.getElementById('modal-footer-area').innerHTML = `
|
||||
<button onclick="closeModal()" class="flex-1 bg-slate-100 py-3.5 rounded-xl font-bold text-sm">취소</button>
|
||||
<button onclick="saveNewUnit()" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
|
||||
<div class="modal-footer-actions">
|
||||
<button onclick="closeModal()" class="modal-btn modal-btn-cancel">취소</button>
|
||||
<button onclick="saveNewUnit()" class="modal-btn modal-btn-save">저장</button>
|
||||
</div>
|
||||
`;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
@@ -798,14 +936,16 @@ function openOrgEditModal(level, oldName) {
|
||||
fieldsArea.style.maxHeight = 'none';
|
||||
fieldsArea.innerHTML = `
|
||||
<div class="col-span-2">
|
||||
<label class="text-[11px] font-black text-slate-400 block">새로운 ${level} 명칭</label>
|
||||
<input id="new-org-name" value="${oldName}" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
|
||||
<label class="member-form-label block">새로운 ${level} 명칭</label>
|
||||
<input id="new-org-name" value="${oldName}" class="member-form-input">
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('modal-footer-area').innerHTML = `
|
||||
<button onclick="deleteOrg('${jsString(level)}', '${jsString(oldName)}')" class="bg-red-50 text-red-600 py-3.5 px-6 rounded-xl font-bold text-sm border border-red-100 hover:bg-red-100 transition-colors">삭제</button>
|
||||
<button onclick="closeModal()" class="flex-1 bg-slate-100 py-3.5 rounded-xl font-bold text-sm">취소</button>
|
||||
<button onclick="saveOrgName('${jsString(level)}', '${jsString(oldName)}')" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
|
||||
<button onclick="deleteOrg('${jsString(level)}', '${jsString(oldName)}')" class="modal-btn modal-btn-delete">삭제</button>
|
||||
<div class="modal-footer-actions">
|
||||
<button onclick="closeModal()" class="modal-btn modal-btn-cancel">취소</button>
|
||||
<button onclick="saveOrgName('${jsString(level)}', '${jsString(oldName)}')" class="modal-btn modal-btn-save">저장</button>
|
||||
</div>
|
||||
`;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
@@ -887,11 +1027,11 @@ function handlePhotoFileChange(event) {
|
||||
|
||||
function renderSeatPreviewCard(seatInfo) {
|
||||
const assigned = Boolean(seatInfo?.assigned);
|
||||
const safeLabel = escapeHtml(seatInfo?.seatLabel || '');
|
||||
const seatMapLabel = String(seatInfo?.seatMapName || '자리배치도').replace(/\s*자리배치도\s*$/u, '').trim() || '사무실';
|
||||
const safeSeatMapName = escapeHtml(seatInfo?.seatMapName || '자리배치도');
|
||||
const safeSlotKey = escapeHtml(seatInfo?.slotKey || '');
|
||||
const safeOfficeLabel = escapeHtml(seatMapLabel);
|
||||
const badge = assigned
|
||||
? `<span class="seat-preview-badge">${safeLabel || '배치완료'}</span>`
|
||||
? `<span class="seat-preview-badge">${safeOfficeLabel}</span>`
|
||||
: '<span class="seat-preview-badge seat-preview-badge-muted">미배치</span>';
|
||||
const body = assigned
|
||||
? `
|
||||
@@ -912,11 +1052,11 @@ function renderSeatPreviewCard(seatInfo) {
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="seat-preview-card">
|
||||
<div class="seat-preview-card${assigned ? ' is-assigned' : ''}">
|
||||
<div class="seat-preview-head">
|
||||
<div>
|
||||
<strong>재석위치</strong>
|
||||
<p>${assigned ? '현재 자리배치도 기준으로 배치된 좌석 정보를 표시합니다.' : '현재 자리배치도에서 배치된 좌석이 없습니다.'}</p>
|
||||
<p>${assigned ? '현재 배치된 사무실과 좌석 위치를 강조해서 표시합니다.' : '현재 자리배치도에서 배치된 좌석이 없습니다.'}</p>
|
||||
</div>
|
||||
${badge}
|
||||
</div>
|
||||
@@ -938,15 +1078,16 @@ async function hydrateMemberSeatPreview(member) {
|
||||
seatLabel: member['자리위치'] || '',
|
||||
slotKey: '',
|
||||
});
|
||||
const layout = await loadActiveSeatMapLayout(true);
|
||||
const layouts = await loadSeatMapLayouts(true);
|
||||
if (!document.getElementById('member-seat-preview')) {
|
||||
return;
|
||||
}
|
||||
const seatInfo = getMemberSeatInfo(layout, member.id) || {
|
||||
seatMapName: layout?.seat_map?.name || '자리배치도',
|
||||
const seatInfo = getMemberSeatInfo(layouts, member.id) || {
|
||||
layout: null,
|
||||
seatMapName: '자리배치도',
|
||||
seatLabel: member['자리위치'] || '',
|
||||
slotKey: '',
|
||||
assigned: Boolean(member['자리위치']),
|
||||
assigned: false,
|
||||
};
|
||||
target.innerHTML = renderSeatPreviewCard(seatInfo);
|
||||
if (!seatInfo.assigned || !seatInfo.seatMapId || !seatInfo.slotKey) {
|
||||
@@ -957,11 +1098,7 @@ async function hydrateMemberSeatPreview(member) {
|
||||
return;
|
||||
}
|
||||
frame.addEventListener('load', () => {
|
||||
if (!frame.contentWindow) {
|
||||
return;
|
||||
}
|
||||
frame.contentWindow.postMessage({ type: 'seatmap-set-mode', mode: 'compact' }, window.location.origin);
|
||||
frame.contentWindow.postMessage({ type: 'seatmap-focus-chair', key: seatInfo.slotKey, padding: 2600 }, window.location.origin);
|
||||
applySeatPreviewFrameState(frame, seatInfo, seatInfo.layout);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
@@ -969,12 +1106,8 @@ function switchModalTab(tab) {
|
||||
const isBasic = tab === 'basic';
|
||||
document.getElementById('modal-sec-basic').classList.toggle('hidden', !isBasic);
|
||||
document.getElementById('modal-sec-org').classList.toggle('hidden', isBasic);
|
||||
document.getElementById('modal-tab-basic').className = isBasic
|
||||
? 'flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all'
|
||||
: 'flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all';
|
||||
document.getElementById('modal-tab-org').className = !isBasic
|
||||
? 'flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all'
|
||||
: 'flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all';
|
||||
document.getElementById('modal-tab-basic').className = isBasic ? 'member-modal-tab is-active' : 'member-modal-tab';
|
||||
document.getElementById('modal-tab-org').className = !isBasic ? 'member-modal-tab is-active' : 'member-modal-tab';
|
||||
}
|
||||
|
||||
function openModal(id) {
|
||||
@@ -991,14 +1124,14 @@ function openModal(id) {
|
||||
fieldsArea.style.maxHeight = 'none';
|
||||
fieldsArea.innerHTML = `
|
||||
<div class="member-detail-top-row">
|
||||
<div class="relative w-32 h-32 rounded-full overflow-hidden border-4 border-indigo-100 shadow-lg">
|
||||
<div class="relative w-32 h-32 rounded-full overflow-hidden border-4 shadow-lg" style="border-color: var(--color-surface-strong);">
|
||||
<img src="${member['사진'] || 'https://via.placeholder.com/120?text=Profile'}" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<div class="member-detail-summary">
|
||||
<div>
|
||||
<h2 class="text-2xl font-black text-slate-800">${member['이름'] || ''}</h2>
|
||||
<p class="text-indigo-600 font-bold">${member['직급'] || '-'} / ${member['직책'] || '팀원'}</p>
|
||||
<p class="text-slate-400 text-xs mt-1 font-medium">${(member._path || []).map((path) => path.name).join(' > ')}</p>
|
||||
<p class="font-bold" style="color: var(--color-header);">${member['직급'] || '-'} / ${member['직책'] || '팀원'}</p>
|
||||
<p class="text-xs mt-1 font-medium" style="color: var(--color-text-muted);">${(member._path || []).map((path) => path.name).join(' > ')}</p>
|
||||
</div>
|
||||
<div class="member-inline-info-grid">
|
||||
<div class="member-inline-info-card">
|
||||
@@ -1016,13 +1149,14 @@ function openModal(id) {
|
||||
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
|
||||
</div>
|
||||
`;
|
||||
footer.innerHTML = '<button onclick="closeModal()" class="w-full bg-slate-800 text-white py-4 rounded-xl font-bold text-sm shadow-lg">닫기</button>';
|
||||
footer.innerHTML = '<button onclick="closeModal()" class="modal-btn modal-btn-close">닫기</button>';
|
||||
modal.style.display = 'flex';
|
||||
hydrateMemberSeatPreview(member);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('modal-title').innerText = id ? '구성원 정보 수정' : '신규 구성원 추가';
|
||||
modal.querySelector('.modal-content').classList.add('wide');
|
||||
fieldsArea.className = 'flex flex-col w-full';
|
||||
fieldsArea.style.maxHeight = 'none';
|
||||
fieldsArea.style.overflowY = 'visible';
|
||||
@@ -1034,14 +1168,14 @@ function openModal(id) {
|
||||
const currentValue = member[field] || '';
|
||||
orgFields += `
|
||||
<div class="col-span-1">
|
||||
<label class="text-[11px] font-black text-slate-600 block">${field}</label>
|
||||
<select id="sel-${field}" onchange="toggleManualInput('${field}')" class="w-full bg-white p-3 rounded-xl border text-sm font-bold text-slate-700 outline-none">
|
||||
<option value="__NEW__" class="text-indigo-600 font-bold">+ 직접/신규 입력</option>
|
||||
<label class="member-form-label block">${field}</label>
|
||||
<select id="sel-${field}" onchange="toggleManualInput('${field}')" class="member-form-select">
|
||||
<option value="__NEW__" class="member-form-new-option">+ 직접/신규 입력</option>
|
||||
<option value="__NONE__" ${currentValue === '' ? 'selected' : ''}>-- 선택 안 함 --</option>
|
||||
${uniqueValues.map((value) => `<option value="${value}" ${value === currentValue ? 'selected' : ''}>${value}</option>`).join('')}
|
||||
</select>
|
||||
<div id="manual-${field}" class="hidden mt-2">
|
||||
<input id="input-${field}" placeholder="직접 입력" class="w-full bg-indigo-50 p-3 rounded-xl border-indigo-200 border text-sm font-bold">
|
||||
<div id="manual-${field}" class="hidden member-form-manual">
|
||||
<input id="input-${field}" placeholder="직접 입력" class="member-form-input">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1050,86 +1184,98 @@ function openModal(id) {
|
||||
const isFlexible = member['근무시간'] === '유연근무제';
|
||||
orgFields += `
|
||||
<div class="col-span-1">
|
||||
<label class="text-[11px] font-black text-slate-600 block">근무 상태</label>
|
||||
<select id="m-status" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none">
|
||||
<option value="근무" ${member['근무상태'] !== '휴직' ? 'selected' : ''}>근무</option>
|
||||
<label class="member-form-label block">근무 상태</label>
|
||||
<select id="m-status" class="member-form-select">
|
||||
<option value="근무" ${member['근무상태'] !== '휴직' && member['근무상태'] !== '퇴직' ? 'selected' : ''}>근무</option>
|
||||
<option value="휴직" ${member['근무상태'] === '휴직' ? 'selected' : ''}>휴직</option>
|
||||
<option value="퇴직" ${member['근무상태'] === '퇴직' ? 'selected' : ''}>퇴직</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<label class="text-[11px] font-black text-slate-600 block">근무 시간</label>
|
||||
<select id="m-worktime" onchange="toggleFlexibleTime(this.value)" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none">
|
||||
<label class="member-form-label block">근무 시간</label>
|
||||
<select id="m-worktime" onchange="toggleFlexibleTime(this.value)" class="member-form-select">
|
||||
<option value="09~18" ${!isFlexible ? 'selected' : ''}>09~18</option>
|
||||
<option value="유연근무제" ${isFlexible ? 'selected' : ''}>유연근무제</option>
|
||||
</select>
|
||||
<div id="flexible-time-area" class="${isFlexible ? '' : 'hidden'} mt-2 flex items-center gap-2">
|
||||
<input type="time" id="m-work-start" value="${member['유연근무_시작'] || '09:00'}" class="bg-indigo-50 p-2 rounded-lg border border-indigo-100 text-xs font-bold w-full">
|
||||
<input type="time" id="m-work-end" value="${member['유연근무_종료'] || '18:00'}" class="bg-indigo-50 p-2 rounded-lg border border-indigo-100 text-xs font-bold w-full">
|
||||
<input type="time" id="m-work-start" value="${member['유연근무_시작'] || '09:00'}" class="member-form-time">
|
||||
<input type="time" id="m-work-end" value="${member['유연근무_종료'] || '18:00'}" class="member-form-time">
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
fieldsArea.innerHTML = `
|
||||
<div class="flex border-b mb-6 sticky top-0 bg-white z-10">
|
||||
<button id="modal-tab-basic" onclick="switchModalTab('basic')" class="flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all">기본 정보</button>
|
||||
<button id="modal-tab-org" onclick="switchModalTab('org')" class="flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all">조직 및 근무</button>
|
||||
<div class="member-modal-tabs">
|
||||
<button id="modal-tab-basic" onclick="switchModalTab('basic')" class="member-modal-tab is-active">기본 정보</button>
|
||||
<button id="modal-tab-org" onclick="switchModalTab('org')" class="member-modal-tab">조직 및 근무</button>
|
||||
</div>
|
||||
<div id="modal-sec-basic" class="grid grid-cols-2 gap-3 modal-form-grid">
|
||||
<div id="modal-sec-basic" class="modal-form-grid member-basic-editor">
|
||||
<input type="hidden" id="m-id" value="${id || ''}">
|
||||
<input type="hidden" id="m-photo-hidden" value="${member['사진'] || ''}">
|
||||
<input type="hidden" id="m-seat-hidden" value="${member['자리위치'] || ''}">
|
||||
<div class="col-span-2 member-basic-top-row">
|
||||
<div class="member-photo-field">
|
||||
<label class="text-[11px] font-black text-slate-600 block">프로필 사진</label>
|
||||
<div class="member-photo-upload-card member-photo-upload-card-compact">
|
||||
<div class="member-photo-preview-wrap">
|
||||
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
|
||||
<div class="member-basic-split">
|
||||
<div class="member-basic-left">
|
||||
<div class="member-photo-panel">
|
||||
<p class="member-modal-panel-title">기본 정보</p>
|
||||
<div class="member-photo-upload-card member-photo-upload-card-inline">
|
||||
<div class="member-photo-card-title">프로필 사진</div>
|
||||
<div class="member-photo-preview-wrap">
|
||||
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
|
||||
</div>
|
||||
<div class="member-photo-upload-controls">
|
||||
<label class="member-photo-file-label" for="m-photo-file">
|
||||
<input id="m-photo-file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onchange="handlePhotoFileChange(event)">
|
||||
<span>사진 파일 선택</span>
|
||||
</label>
|
||||
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-photo-upload-controls">
|
||||
<label class="member-photo-file-label" for="m-photo-file">
|
||||
<input id="m-photo-file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onchange="handlePhotoFileChange(event)">
|
||||
<span>사진 파일 선택</span>
|
||||
</label>
|
||||
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
|
||||
</div>
|
||||
<div class="member-basic-fields member-modal-panel">
|
||||
<p class="member-modal-panel-title">기본 정보</p>
|
||||
<div class="member-basic-field">
|
||||
<label class="member-form-label block">이름 (필수)</label>
|
||||
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="member-form-input">
|
||||
</div>
|
||||
<div class="member-basic-field">
|
||||
<label class="member-form-label block">사번</label>
|
||||
<input id="m-employee-id" value="${member['사번'] || ''}" class="member-form-input">
|
||||
</div>
|
||||
<div class="member-basic-field">
|
||||
<label class="member-form-label block">전화번호</label>
|
||||
<input id="m-phone" value="${member['전화번호'] || ''}" class="member-form-input">
|
||||
</div>
|
||||
<div class="member-basic-field">
|
||||
<label class="member-form-label block">이메일</label>
|
||||
<input id="m-email" value="${member['이메일'] || ''}" class="member-form-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-name-field">
|
||||
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
|
||||
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
|
||||
<div class="member-inline-info-grid member-inline-info-grid-edit">
|
||||
<div class="member-inline-info-card">
|
||||
<label>사번</label>
|
||||
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||
</div>
|
||||
<div class="member-inline-info-card">
|
||||
<label>전화번호</label>
|
||||
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||
</div>
|
||||
<div class="member-inline-info-card member-inline-info-card-full">
|
||||
<label>이메일</label>
|
||||
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||
</div>
|
||||
<div class="member-basic-right">
|
||||
<p class="member-modal-panel-title" style="padding:16px 16px 0;">조직 및 근무</p>
|
||||
<div class="member-seat-field member-seat-field-compact">
|
||||
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-[11px] font-black text-slate-600 block mb-2">자리 위치</label>
|
||||
${renderSeatPreviewCard(member['자리위치'] || '')}
|
||||
</div>
|
||||
</div>
|
||||
${orgFields}
|
||||
<div class="member-modal-panel">${orgFields}</div>
|
||||
`;
|
||||
|
||||
resetPhotoPreviewObjectUrl();
|
||||
|
||||
const deleteBtn = id ? `<button onclick="deleteMember('${id}')" class="bg-red-50 text-red-600 py-3.5 px-6 rounded-xl font-bold text-sm border border-red-100 hover:bg-red-100 transition-colors">삭제</button>` : '';
|
||||
const deleteBtn = id ? `<button onclick="deleteMember('${id}')" class="modal-btn modal-btn-delete">삭제</button>` : '';
|
||||
footer.innerHTML = `
|
||||
${deleteBtn}
|
||||
<button onclick="closeModal()" class="flex-1 bg-slate-100 py-3.5 rounded-xl font-bold text-sm">취소</button>
|
||||
<button onclick="saveMember()" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
|
||||
<div class="modal-footer-actions">
|
||||
<button onclick="closeModal()" class="modal-btn modal-btn-cancel">취소</button>
|
||||
<button onclick="saveMember()" class="modal-btn modal-btn-save">저장</button>
|
||||
</div>
|
||||
`;
|
||||
modal.style.display = 'flex';
|
||||
if (id) {
|
||||
hydrateMemberSeatPreview(member);
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
@@ -1243,6 +1389,14 @@ function openListViewModal(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const defaultDate = getDefaultHistoryDate();
|
||||
listViewState.mode = 'current';
|
||||
listViewState.snapshotDate = defaultDate;
|
||||
listViewState.compareFromDate = defaultDate;
|
||||
listViewState.compareToDate = defaultDate;
|
||||
listViewState.snapshotMembers = [];
|
||||
listViewState.compareItems = [];
|
||||
|
||||
const modal = document.getElementById('modal');
|
||||
modal.querySelector('.modal-content').classList.add('wide');
|
||||
document.getElementById('modal-title').innerText = '인원 명단';
|
||||
@@ -1252,36 +1406,41 @@ function openListViewModal(event) {
|
||||
isListMode = true;
|
||||
editingMembers = cloneMembers(members);
|
||||
fieldsArea.innerHTML = `
|
||||
<div class="mb-4 flex gap-2 p-1">
|
||||
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-slate-50 border-2 border-slate-100 p-3 rounded-xl text-sm outline-none font-bold focus:border-indigo-400 transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
|
||||
<button onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-indigo-600 text-white px-5 rounded-xl font-bold text-sm">검색</button>
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-group">
|
||||
<button type="button" onclick="showCurrentListView()" class="list-mode-btn">현재 명단</button>
|
||||
</div>
|
||||
<div class="list-toolbar-divider" aria-hidden="true"></div>
|
||||
<div class="list-toolbar-group list-date-group">
|
||||
<input type="date" id="list-snapshot-date" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
||||
<button type="button" onclick="loadSnapshotListView()" class="list-mode-btn">기준일 조회</button>
|
||||
</div>
|
||||
<div class="list-toolbar-divider" aria-hidden="true"></div>
|
||||
<div class="list-toolbar-group list-date-group">
|
||||
<input type="date" id="list-compare-from" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
||||
<span class="list-date-separator">~</span>
|
||||
<input type="date" id="list-compare-to" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
||||
<button type="button" onclick="loadCompareListView()" class="list-mode-btn">변경 비교</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-toolbar-row">
|
||||
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-[#f6eddd] border-2 border-[#e0d0b4] p-3 rounded-xl text-sm outline-none font-bold text-[#1f2f25] focus:border-[#d68a3a] transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
|
||||
<button type="button" onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-[#214634] text-white px-5 rounded-xl font-bold text-sm">검색</button>
|
||||
</div>
|
||||
<div id="list-view-status" class="list-view-status"></div>
|
||||
</div>
|
||||
<div id="list-table-container" class="overflow-y-auto flex-1 border rounded-xl"></div>
|
||||
`;
|
||||
renderListViewTable();
|
||||
|
||||
const footer = document.getElementById('modal-footer-area');
|
||||
if (isAdmin) {
|
||||
footer.innerHTML = `
|
||||
<div class="flex gap-2 w-full justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
<button onclick="openAddModal(event)" class="bg-indigo-50 text-indigo-600 px-4 py-2 rounded-lg text-xs font-bold border border-indigo-100">+ 구성원 추가</button>
|
||||
<button onclick="openUnitAddModal(event)" class="bg-indigo-50 text-indigo-600 px-4 py-2 rounded-lg text-xs font-bold border border-indigo-100">+ 조직 추가</button>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<p class="text-[10px] text-slate-400 font-bold mr-4">항목을 드래그하여 순서를 바꿀 수 있습니다.</p>
|
||||
<button onclick="closeModal()" class="bg-slate-100 text-slate-600 px-6 py-2 rounded-lg text-xs font-bold">취소</button>
|
||||
<button onclick="applyListViewChanges()" class="bg-indigo-600 text-white px-8 py-2 rounded-lg text-xs font-bold shadow-lg shadow-indigo-200">반영하기</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
footer.innerHTML = '<div class="flex gap-2 w-full justify-end items-center"><button onclick="closeModal()" class="bg-indigo-600 text-white px-10 py-2.5 rounded-lg text-xs font-bold shadow-lg shadow-indigo-200">닫기</button></div>';
|
||||
}
|
||||
renderListViewModalContent();
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
async function applyListViewChanges() {
|
||||
if (listViewState.mode !== 'current') {
|
||||
closeModal();
|
||||
return;
|
||||
}
|
||||
if (!confirm('리스트의 변경 사항을 메인 화면에 반영하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
@@ -1290,19 +1449,192 @@ async function applyListViewChanges() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function renderListViewFooter() {
|
||||
const footer = document.getElementById('modal-footer-area');
|
||||
if (!footer) {
|
||||
return;
|
||||
}
|
||||
if (listViewState.mode === 'current' && isAdmin) {
|
||||
footer.innerHTML = `
|
||||
<div class="flex gap-2 w-full justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
<button onclick="openAddModal(event)" class="bg-[#f6eddd] text-[#214634] px-4 py-2 rounded-lg text-xs font-bold border border-[#e0d0b4]">+ 구성원 추가</button>
|
||||
<button onclick="openUnitAddModal(event)" class="bg-[#f6eddd] text-[#214634] px-4 py-2 rounded-lg text-xs font-bold border border-[#e0d0b4]">+ 조직 추가</button>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<p class="text-[10px] text-[#8b8a77] font-bold mr-4">항목을 드래그하여 순서를 바꿀 수 있습니다.</p>
|
||||
<button onclick="closeModal()" class="bg-[#efe4d0] text-[#5b665a] px-6 py-2 rounded-lg text-xs font-bold">취소</button>
|
||||
<button onclick="applyListViewChanges()" class="bg-[#214634] text-white px-8 py-2 rounded-lg text-xs font-bold shadow-lg shadow-[#d6c1a3]">반영하기</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
footer.innerHTML = '<div class="flex gap-2 w-full justify-end items-center"><button onclick="closeModal()" class="bg-[#214634] text-white px-10 py-2.5 rounded-lg text-xs font-bold shadow-lg shadow-[#d6c1a3]">닫기</button></div>';
|
||||
}
|
||||
|
||||
function getRenderableListMembers() {
|
||||
if (listViewState.mode === 'snapshot') {
|
||||
return listViewState.snapshotMembers;
|
||||
}
|
||||
return editingMembers;
|
||||
}
|
||||
|
||||
function getListSearchEntries() {
|
||||
if (listViewState.mode === 'compare') {
|
||||
return (listViewState.compareItems || []).map((item) => ({
|
||||
rowId: `list-compare-row-${item.member_id}`,
|
||||
name: String(item.name || ''),
|
||||
values: [String(item.name || ''), ...(item.before_lines || []), ...(item.after_lines || [])],
|
||||
}));
|
||||
}
|
||||
return getRenderableListMembers().map((member) => ({
|
||||
rowId: `list-row-${member._id}`,
|
||||
name: String(member['이름'] || ''),
|
||||
values: [
|
||||
String(member['이름'] || ''),
|
||||
...levelOrder.map((level) => String(member[level] || '')),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
function formatCompareChangedAt(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) {
|
||||
return '-';
|
||||
}
|
||||
const date = new Date(raw);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return raw;
|
||||
}
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function renderListViewCompareTable() {
|
||||
const container = document.getElementById('list-table-container');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = listViewState.compareItems || [];
|
||||
let html = `
|
||||
<table class="list-table list-compare-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-name">이름</th>
|
||||
<th class="col-compare-status">상태</th>
|
||||
<th class="col-compare-date">변경일시</th>
|
||||
<th class="col-compare-category">변경유형</th>
|
||||
<th>이전</th>
|
||||
<th>현재</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
if (!rows.length) {
|
||||
html += '<tr><td colspan="6" class="list-empty-cell">선택한 기간 사이의 구성원 변경 내역이 없습니다.</td></tr>';
|
||||
} else {
|
||||
rows.forEach((item) => {
|
||||
const categories = (item.categories || []).map((category) => `<span class="list-compare-chip">${escapeHtml(category)}</span>`).join('');
|
||||
const beforeLines = (item.before_lines || []).map((line) => `<div class="list-compare-line">${escapeHtml(line)}</div>`).join('') || '<span class="text-slate-300">-</span>';
|
||||
const afterLines = (item.after_lines || []).map((line) => `<div class="list-compare-line">${escapeHtml(line)}</div>`).join('') || '<span class="text-slate-300">-</span>';
|
||||
html += `
|
||||
<tr id="list-compare-row-${item.member_id}">
|
||||
<td class="font-black text-slate-700">${escapeHtml(item.name || '-')}</td>
|
||||
<td><span class="list-compare-status list-compare-status-${escapeHtml(item.status || 'updated')}">${escapeHtml(item.status_label || '-')}</span></td>
|
||||
<td>${escapeHtml(formatCompareChangedAt(item.changed_at))}</td>
|
||||
<td><div class="list-compare-chip-group">${categories || '<span class="text-slate-300">-</span>'}</div></td>
|
||||
<td class="list-compare-cell">${beforeLines}</td>
|
||||
<td class="list-compare-cell">${afterLines}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderListViewModalContent() {
|
||||
const status = document.getElementById('list-view-status');
|
||||
if (status) {
|
||||
if (listViewState.mode === 'snapshot') {
|
||||
status.textContent = listViewState.snapshotDate
|
||||
? `${listViewState.snapshotDate} 기준 인원 명단입니다.`
|
||||
: '기준일을 선택한 뒤 조회하세요.';
|
||||
} else if (listViewState.mode === 'compare') {
|
||||
status.textContent = (listViewState.compareFromDate && listViewState.compareToDate)
|
||||
? `${listViewState.compareFromDate} ~ ${listViewState.compareToDate} 변경 내역입니다.`
|
||||
: '비교 시작일과 종료일을 선택하세요.';
|
||||
} else {
|
||||
status.textContent = '현재 조직 인원 명단입니다.';
|
||||
}
|
||||
}
|
||||
|
||||
if (listViewState.mode === 'compare') {
|
||||
renderListViewCompareTable();
|
||||
} else {
|
||||
renderListViewTable();
|
||||
}
|
||||
renderListViewFooter();
|
||||
}
|
||||
|
||||
function showCurrentListView() {
|
||||
listViewState.mode = 'current';
|
||||
renderListViewModalContent();
|
||||
}
|
||||
|
||||
async function loadSnapshotListView() {
|
||||
const snapshotDate = document.getElementById('list-snapshot-date')?.value || '';
|
||||
if (!snapshotDate) {
|
||||
alert('기준일을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
const payload = await apiFetch(`/api/members?as_of=${encodeURIComponent(snapshotDate)}`);
|
||||
listViewState.snapshotDate = snapshotDate;
|
||||
listViewState.snapshotMembers = getVisibleLegacyMembers((payload.items || []).map(toLegacyMember));
|
||||
listViewState.mode = 'snapshot';
|
||||
renderListViewModalContent();
|
||||
}
|
||||
|
||||
async function loadCompareListView() {
|
||||
const fromDate = document.getElementById('list-compare-from')?.value || '';
|
||||
const toDate = document.getElementById('list-compare-to')?.value || '';
|
||||
if (!fromDate || !toDate) {
|
||||
alert('비교 시작일과 종료일을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
const payload = await apiFetch(`/api/history/members/compare?from_date=${encodeURIComponent(fromDate)}&to_date=${encodeURIComponent(toDate)}`);
|
||||
listViewState.compareFromDate = fromDate;
|
||||
listViewState.compareToDate = toDate;
|
||||
listViewState.compareItems = Array.isArray(payload.items) ? payload.items : [];
|
||||
listViewState.mode = 'compare';
|
||||
renderListViewModalContent();
|
||||
}
|
||||
|
||||
function renderListViewTable() {
|
||||
const container = document.getElementById('list-table-container');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<table class="list-table"><thead><tr>${isAdmin ? '<th width="40">순서</th>' : ''}<th class="col-name">이름</th><th class="col-rank">직급</th><th class="col-pos">직책</th><th class="col-unit-sm">셀</th><th class="col-unit-sm">팀</th><th class="col-unit-lg">디비전</th><th class="col-unit-lg">그룹</th><th class="col-unit-lg">부서</th><th class="col-corp">소속</th><th class="col-action">${isAdmin ? '관리' : '조회'}</th></tr></thead><tbody id="list-body">`;
|
||||
const sourceMembers = getRenderableListMembers();
|
||||
const editable = isAdmin && listViewState.mode === 'current';
|
||||
const inspectable = !editable && listViewState.mode === 'current';
|
||||
const groupColumnCount = editable ? 11 : 10;
|
||||
let html = `<table class="list-table"><thead><tr>${editable ? '<th width="40">순서</th>' : ''}<th class="col-name">이름</th><th class="col-rank">직급</th><th class="col-pos">직책</th><th class="col-unit-sm">셀</th><th class="col-unit-sm">팀</th><th class="col-unit-lg">디비전</th><th class="col-unit-lg">그룹</th><th class="col-unit-lg">부서</th><th class="col-corp">소속</th><th class="col-action">${editable ? '관리' : '조회'}</th></tr></thead><tbody id="list-body">`;
|
||||
const lastValues = {};
|
||||
levelOrder.forEach((level) => {
|
||||
lastValues[level] = '';
|
||||
});
|
||||
|
||||
editingMembers.forEach((member, index) => {
|
||||
sourceMembers.forEach((member, index) => {
|
||||
let isAnyParentCollapsed = false;
|
||||
levelOrder.forEach((level, depth) => {
|
||||
const value = (member[level] || '').trim();
|
||||
@@ -1316,8 +1648,8 @@ function renderListViewTable() {
|
||||
}
|
||||
if (value !== lastValues[level]) {
|
||||
const isCollapsed = collapsedUnits.has(key);
|
||||
const dragAttr = isAdmin ? `draggable="true" ondragstart="handleListGroupDragStart(event, '${jsString(level)}', '${jsString(value)}')" ondragover="event.preventDefault()" ondrop="handleListGroupDrop(event, '${jsString(level)}', '${jsString(value)}')"` : '';
|
||||
html += `<tr ${dragAttr} class="list-header-row lvl-${depth} ${isCollapsed ? 'collapsed' : ''} ${isAnyParentCollapsed ? 'hidden-row' : ''}"><td colspan="${(isAdmin ? 10 : 9) + 1}" onclick="toggleUnitCollapse('${jsString(level)}', '${jsString(value)}')" style="padding-left: 15px !important;"><span class="collapse-icon">▼</span> ${value}</td></tr>`;
|
||||
const dragAttr = editable ? `draggable="true" ondragstart="handleListGroupDragStart(event, '${jsString(level)}', '${jsString(value)}')" ondragover="event.preventDefault()" ondrop="handleListGroupDrop(event, '${jsString(level)}', '${jsString(value)}')"` : '';
|
||||
html += `<tr ${dragAttr} class="list-header-row lvl-${depth} ${isCollapsed ? 'collapsed' : ''} ${isAnyParentCollapsed ? 'hidden-row' : ''}"><td colspan="${groupColumnCount}" onclick="toggleUnitCollapse('${jsString(level)}', '${jsString(value)}')" style="padding-left: 15px !important;"><span class="collapse-icon">▼</span> ${escapeHtml(value)}</td></tr>`;
|
||||
lastValues[level] = value;
|
||||
levelOrder.slice(depth + 1).forEach((childLevel) => {
|
||||
lastValues[childLevel] = '';
|
||||
@@ -1326,20 +1658,25 @@ function renderListViewTable() {
|
||||
});
|
||||
|
||||
const hidden = levelOrder.some((level) => member[level] && collapsedUnits.has(`${level}_${member[level].trim()}`)) || isAnyParentCollapsed;
|
||||
const rowDragAttr = isAdmin ? `draggable="true" ondragstart="handleListDragStart(event, ${index})" ondragover="event.preventDefault()" ondrop="handleListDrop(event, ${index})"` : '';
|
||||
const rowDragAttr = editable ? `draggable="true" ondragstart="handleListDragStart(event, ${index})" ondragover="event.preventDefault()" ondrop="handleListDrop(event, ${index})"` : '';
|
||||
const actionCell = editable
|
||||
? `<div class="flex gap-1 justify-center"><span class="list-action-btn btn-edit" onclick="openModal('${member._id}')">수정</span><span class="list-action-btn btn-delete" onclick="deleteMember('${member._id}')">삭제</span></div>`
|
||||
: inspectable
|
||||
? `<span class="list-action-btn btn-edit bg-indigo-50 text-indigo-600 border border-indigo-100" onclick="openModal('${member._id}')">조회</span>`
|
||||
: '<span class="text-slate-300">-</span>';
|
||||
html += `
|
||||
<tr id="list-row-${member._id}" ${rowDragAttr} class="${hidden ? 'hidden-row' : ''}">
|
||||
${isAdmin ? '<td class="text-slate-300 cursor-move">☰</td>' : ''}
|
||||
<td class="font-black text-slate-700">${member['이름']}</td>
|
||||
<td>${member['직급'] || '-'}</td>
|
||||
<td>${member['근무상태'] === '휴직' ? '<span class="text-red-500 font-black">휴직</span>' : (member['직책'] || '-')}</td>
|
||||
<td>${member['셀'] || '-'}</td>
|
||||
<td>${member['팀'] || '-'}</td>
|
||||
<td>${member['디비전'] || '-'}</td>
|
||||
<td>${member['그룹'] || '-'}</td>
|
||||
<td>${member['부서'] || '-'}</td>
|
||||
<td>${member['소속회사'] || '-'}</td>
|
||||
<td>${isAdmin ? `<div class="flex gap-1 justify-center"><span class="list-action-btn btn-edit" onclick="openModal('${member._id}')">수정</span><span class="list-action-btn btn-delete" onclick="deleteMember('${member._id}')">삭제</span></div>` : `<span class="list-action-btn btn-edit bg-indigo-50 text-indigo-600 border border-indigo-100" onclick="openModal('${member._id}')">조회</span>`}</td>
|
||||
${editable ? '<td class="text-slate-300 cursor-move">☰</td>' : ''}
|
||||
<td class="font-black text-slate-700">${escapeHtml(member['이름'] || '-')}</td>
|
||||
<td>${escapeHtml(member['직급'] || '-')}</td>
|
||||
<td>${member['근무상태'] === '휴직' ? '<span class="text-red-500 font-black">휴직</span>' : escapeHtml(member['직책'] || '-')}</td>
|
||||
<td>${escapeHtml(member['셀'] || '-')}</td>
|
||||
<td>${escapeHtml(member['팀'] || '-')}</td>
|
||||
<td>${escapeHtml(member['디비전'] || '-')}</td>
|
||||
<td>${escapeHtml(member['그룹'] || '-')}</td>
|
||||
<td>${escapeHtml(member['부서'] || '-')}</td>
|
||||
<td>${escapeHtml(member['소속회사'] || '-')}</td>
|
||||
<td>${actionCell}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
@@ -1389,15 +1726,14 @@ function handleListSearch(value) {
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('.list-search-target').forEach((element) => element.classList.remove('list-search-target'));
|
||||
const targetMember = editingMembers.find((member) => (
|
||||
(member['이름'] || '').toLowerCase().includes(query)
|
||||
|| levelOrder.some((level) => (member[level] || '').toLowerCase().includes(query))
|
||||
const targetEntry = getListSearchEntries().find((entry) => (
|
||||
entry.values.some((candidate) => String(candidate || '').toLowerCase().includes(query))
|
||||
));
|
||||
if (!targetMember) {
|
||||
if (!targetEntry) {
|
||||
alert('검색 결과가 없습니다.');
|
||||
return;
|
||||
}
|
||||
const row = document.getElementById(`list-row-${targetMember._id}`);
|
||||
const row = document.getElementById(targetEntry.rowId);
|
||||
if (row) {
|
||||
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
row.classList.add('list-search-target');
|
||||
|
||||
Binary file not shown.
@@ -25,6 +25,13 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /integrations/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
56
scripts/prepare_dev_worktree.sh
Executable file
56
scripts/prepare_dev_worktree.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DEV_DIR="${DEV_DIR:-${ROOT_DIR}/.dev-worktree-8081}"
|
||||
TARGET_REF="${1:-HEAD}"
|
||||
FORCE_RECREATE="${FORCE_RECREATE:-0}"
|
||||
|
||||
copy_optional_path() {
|
||||
local rel_path="$1"
|
||||
local src="${ROOT_DIR}/${rel_path}"
|
||||
local dst="${DEV_DIR}/${rel_path}"
|
||||
if [[ ! -e "${src}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$(dirname "${dst}")"
|
||||
cp -a "${src}" "${dst}"
|
||||
}
|
||||
|
||||
if [[ "${DEV_DIR}" == "${ROOT_DIR}" ]]; then
|
||||
echo "DEV_DIR must not be the same as the production workspace." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "${DEV_DIR}/.git" && "${FORCE_RECREATE}" != "1" ]]; then
|
||||
echo "[1/6] Reusing existing dev workspace at ${DEV_DIR}"
|
||||
else
|
||||
echo "[1/6] Removing previous dev workspace at ${DEV_DIR}"
|
||||
rm -rf "${DEV_DIR}"
|
||||
|
||||
echo "[2/6] Cloning production workspace into isolated dev workspace"
|
||||
git clone --no-hardlinks "${ROOT_DIR}" "${DEV_DIR}" >/dev/null
|
||||
|
||||
echo "[3/6] Checking out detached ref ${TARGET_REF}"
|
||||
git -C "${DEV_DIR}" checkout --detach "${TARGET_REF}" >/dev/null
|
||||
fi
|
||||
|
||||
echo "[4/6] Copying local runtime env when available"
|
||||
copy_optional_path ".env"
|
||||
|
||||
echo "[5/6] Copying local-only incoming design assets when available"
|
||||
copy_optional_path "incoming-files/1.png"
|
||||
copy_optional_path "incoming-files/260320.html"
|
||||
copy_optional_path "incoming-files/sample style.css"
|
||||
copy_optional_path "incoming-files/seat/center_chair_people_map(2).html"
|
||||
copy_optional_path "incoming-files/사업관리대장"
|
||||
|
||||
echo "[6/6] Dev worktree ready"
|
||||
echo "Path: ${DEV_DIR}"
|
||||
echo "Use this to start 8081 from the isolated workspace:"
|
||||
echo " cd ${DEV_DIR} && docker compose -p mh-dashboard-organization-dev --env-file .env -f docker-compose.8081.yml up -d --build"
|
||||
if [[ "${FORCE_RECREATE}" != "1" ]]; then
|
||||
echo "To fully rebuild the dev workspace, run:"
|
||||
echo " FORCE_RECREATE=1 ./scripts/prepare_dev_worktree.sh"
|
||||
fi
|
||||
15
scripts/start_8081.sh
Executable file
15
scripts/start_8081.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DEV_DIR="${DEV_DIR:-${ROOT_DIR}/.dev-worktree-8081}"
|
||||
|
||||
"${ROOT_DIR}/scripts/prepare_dev_worktree.sh"
|
||||
|
||||
cd "${DEV_DIR}"
|
||||
docker compose -p mh-dashboard-organization-dev --env-file .env -f docker-compose.8081.yml up -d --build
|
||||
|
||||
echo "8081 started from ${DEV_DIR}"
|
||||
echo "Verify mounts with:"
|
||||
echo " docker inspect mh-dashboard-organization-dev-backend-1 --format '{{range .Mounts}}{{println .Source \"->\" .Destination}}{{end}}'"
|
||||
12
scripts/start_local_dashboards.sh
Executable file
12
scripts/start_local_dashboards.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
docker compose up -d
|
||||
"${ROOT_DIR}/scripts/start_8081.sh"
|
||||
|
||||
echo "8080: http://localhost:8080"
|
||||
echo "8081: http://localhost:8081"
|
||||
260
scripts/sync_prod_db_to_dev.sh
Executable file
260
scripts/sync_prod_db_to_dev.sh
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PROD_DIR="${ROOT_DIR}"
|
||||
DEV_DIR="${DEV_DIR:-/tmp/mh-dashboard-organization-dev-worktree}"
|
||||
DEV_PROJECT_NAME="${DEV_PROJECT_NAME:-mh-dashboard-organization-dev}"
|
||||
DEV_COMPOSE_FILE="${DEV_COMPOSE_FILE:-${DEV_DIR}/docker-compose.8081.yml}"
|
||||
SCOPE="${1:-minimal}"
|
||||
|
||||
if [[ ! -f "${PROD_DIR}/docker-compose.yml" ]]; then
|
||||
echo "Production workspace not found: ${PROD_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${DEV_DIR}/docker-compose.yml" ]]; then
|
||||
echo "Development workspace not found: ${DEV_DIR}" >&2
|
||||
echo "Set DEV_DIR=/path/to/workspace if the dev workspace moved." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${DEV_COMPOSE_FILE}" ]]; then
|
||||
echo "Development compose file not found: ${DEV_COMPOSE_FILE}" >&2
|
||||
echo "Set DEV_COMPOSE_FILE=/path/to/dev-compose.yml if the dev compose file moved." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${SCOPE}" in
|
||||
minimal)
|
||||
TABLES=(
|
||||
member_aliases
|
||||
member_overrides
|
||||
member_retirements
|
||||
members
|
||||
seat_maps
|
||||
seat_slots
|
||||
)
|
||||
;;
|
||||
analysis)
|
||||
TABLES=(
|
||||
integration_import_batches
|
||||
integration_raw_organization_rows
|
||||
integration_raw_mh_rows
|
||||
integration_raw_mh_pm_rows
|
||||
integration_raw_payment_rows
|
||||
integration_project_aliases
|
||||
integration_project_category_mappings
|
||||
integration_project_pm_assignments
|
||||
integration_projects
|
||||
integration_work_logs
|
||||
integration_work_log_segments
|
||||
integration_vouchers
|
||||
)
|
||||
;;
|
||||
full)
|
||||
TABLES=(
|
||||
integration_import_batches
|
||||
integration_raw_organization_rows
|
||||
integration_raw_mh_rows
|
||||
integration_raw_mh_pm_rows
|
||||
integration_raw_payment_rows
|
||||
integration_project_aliases
|
||||
integration_project_category_mappings
|
||||
integration_project_pm_assignments
|
||||
integration_projects
|
||||
integration_work_logs
|
||||
integration_work_log_segments
|
||||
integration_vouchers
|
||||
member_aliases
|
||||
member_overrides
|
||||
member_retirements
|
||||
members
|
||||
seat_maps
|
||||
seat_slots
|
||||
)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [minimal|analysis|full]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
PROD_COMPOSE=(docker compose --project-directory "${PROD_DIR}")
|
||||
DEV_COMPOSE=(docker compose -p "${DEV_PROJECT_NAME}" --env-file "${DEV_DIR}/.env" -f "${DEV_COMPOSE_FILE}")
|
||||
|
||||
run_compose() {
|
||||
local dir="$1"
|
||||
shift
|
||||
(cd "${dir}" && "$@")
|
||||
}
|
||||
|
||||
require_service() {
|
||||
local dir="$1"
|
||||
shift
|
||||
run_compose "${dir}" "$@" >/dev/null
|
||||
}
|
||||
|
||||
echo "[1/8] Checking source and target stacks"
|
||||
require_service "${PROD_DIR}" "${PROD_COMPOSE[@]}" ps
|
||||
require_service "${DEV_DIR}" "${DEV_COMPOSE[@]}" ps
|
||||
|
||||
echo "[2/8] Ensuring db containers are reachable"
|
||||
run_compose "${PROD_DIR}" "${PROD_COMPOSE[@]}" exec -T db pg_isready -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" >/dev/null
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T db pg_isready -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" >/dev/null
|
||||
|
||||
echo "[3/8] Pausing 8081 app services to avoid partial reads during sync"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" stop proxy frontend backend >/dev/null
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "${WORK_DIR}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
DUMP_FILE="${WORK_DIR}/prod_to_dev_${SCOPE}.sql"
|
||||
TRUNCATE_FILE="${WORK_DIR}/truncate_${SCOPE}.sql"
|
||||
SEAT_POSITIONS_FILE="${WORK_DIR}/seat_positions.csv"
|
||||
SEQUENCE_FIX_FILE="${WORK_DIR}/sequence_fix.sql"
|
||||
|
||||
echo "[4/8] Building truncate script for ${SCOPE} scope"
|
||||
{
|
||||
echo "BEGIN;"
|
||||
echo "SET session_replication_role = replica;"
|
||||
printf 'TRUNCATE TABLE %s RESTART IDENTITY CASCADE;\n' "$(printf 'public.%s,' "${TABLES[@]}" | sed 's/,$//')"
|
||||
echo "SET session_replication_role = DEFAULT;"
|
||||
echo "COMMIT;"
|
||||
} > "${TRUNCATE_FILE}"
|
||||
|
||||
echo "[5/8] Dumping ${SCOPE} data from 8080 source DB"
|
||||
TABLE_ARGS=()
|
||||
for table in "${TABLES[@]}"; do
|
||||
TABLE_ARGS+=(-t "public.${table}")
|
||||
done
|
||||
run_compose "${PROD_DIR}" "${PROD_COMPOSE[@]}" exec -T db \
|
||||
pg_dump -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
|
||||
--data-only --column-inserts --disable-triggers --no-owner --no-privileges \
|
||||
"${TABLE_ARGS[@]}" > "${DUMP_FILE}"
|
||||
|
||||
echo "[5.5/8] Exporting seat_positions in portable format"
|
||||
run_compose "${PROD_DIR}" "${PROD_COMPOSE[@]}" exec -T db \
|
||||
psql -At -F ',' -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
|
||||
-c "COPY (
|
||||
SELECT member_id, seat_map_id, seat_slot_id, row_index, col_index, seat_label, updated_at
|
||||
FROM public.seat_positions
|
||||
ORDER BY member_id
|
||||
) TO STDOUT WITH CSV" > "${SEAT_POSITIONS_FILE}"
|
||||
|
||||
echo "[6/8] Truncating target tables in 8081 dev DB"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T db \
|
||||
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" >/dev/null < "${TRUNCATE_FILE}"
|
||||
|
||||
echo "[7/8] Restoring dumped data into 8081 dev DB"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T db \
|
||||
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" >/dev/null < "${DUMP_FILE}"
|
||||
|
||||
echo "[7.5/8] Restoring portable seat_positions and rebuilding auth users"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T db \
|
||||
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
|
||||
-c "DELETE FROM public.seat_positions" >/dev/null
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T db \
|
||||
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
|
||||
-c "COPY public.seat_positions (member_id, seat_map_id, seat_slot_id, row_index, col_index, seat_label, updated_at) FROM STDIN WITH CSV" >/dev/null < "${SEAT_POSITIONS_FILE}"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" up -d backend >/dev/null
|
||||
AUTH_SYNC_PY="$(cat <<'PY'
|
||||
from backend.app.main import get_conn, sync_auth_users_from_members
|
||||
from backend.app.db import ensure_history_backfill
|
||||
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("UPDATE members SET seat_label = ''")
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE members AS m
|
||||
SET seat_label = sp.seat_label
|
||||
FROM seat_positions AS sp
|
||||
WHERE sp.member_id = m.id
|
||||
"""
|
||||
)
|
||||
sync_auth_users_from_members(cur)
|
||||
ensure_history_backfill(cur)
|
||||
conn.commit()
|
||||
print("members, seat labels, auth users, and history backfill synced")
|
||||
PY
|
||||
)"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T backend python -c "${AUTH_SYNC_PY}"
|
||||
|
||||
echo "[7.8/8] Resetting serial sequences"
|
||||
{
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.members', 'id'), COALESCE((SELECT MAX(id) FROM public.members), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.member_aliases', 'id'), COALESCE((SELECT MAX(id) FROM public.member_aliases), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.member_overrides', 'id'), COALESCE((SELECT MAX(id) FROM public.member_overrides), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.member_retirements', 'id'), COALESCE((SELECT MAX(id) FROM public.member_retirements), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.seat_maps', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_maps), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.seat_slots', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_slots), 1), true);"
|
||||
if [[ "${SCOPE}" == "analysis" || "${SCOPE}" == "full" ]]; then
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_import_batches', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_import_batches), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_organization_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_organization_rows), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_mh_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_mh_rows), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_mh_pm_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_mh_pm_rows), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_payment_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_payment_rows), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_project_aliases', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_project_aliases), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_project_category_mappings', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_project_category_mappings), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_project_pm_assignments', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_project_pm_assignments), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_projects', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_projects), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_work_logs', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_work_logs), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_work_log_segments', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_work_log_segments), 1), true);"
|
||||
echo "SELECT setval(pg_get_serial_sequence('public.integration_vouchers', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_vouchers), 1), true);"
|
||||
fi
|
||||
} > "${SEQUENCE_FIX_FILE}"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T db \
|
||||
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" >/dev/null < "${SEQUENCE_FIX_FILE}"
|
||||
|
||||
VERIFY_SQL="${WORK_DIR}/verify_${SCOPE}.sql"
|
||||
{
|
||||
cat <<'SQL'
|
||||
SELECT 'members' AS table_name, COUNT(*)::text AS value FROM public.members
|
||||
UNION ALL
|
||||
SELECT 'member_retirements', COUNT(*)::text FROM public.member_retirements
|
||||
UNION ALL
|
||||
SELECT 'seat_maps', COUNT(*)::text FROM public.seat_maps
|
||||
UNION ALL
|
||||
SELECT 'seat_slots', COUNT(*)::text FROM public.seat_slots
|
||||
UNION ALL
|
||||
SELECT 'seat_positions', COUNT(*)::text FROM public.seat_positions
|
||||
UNION ALL
|
||||
SELECT 'members_with_seat_label', COUNT(*)::text FROM public.members WHERE COALESCE(seat_label, '') <> ''
|
||||
UNION ALL
|
||||
SELECT 'seat_positions_without_slot', COUNT(*)::text FROM public.seat_positions WHERE seat_slot_id IS NULL
|
||||
UNION ALL
|
||||
SELECT 'seat_label_mismatch', COUNT(*)::text
|
||||
FROM public.members m
|
||||
JOIN public.seat_positions sp ON sp.member_id = m.id
|
||||
WHERE COALESCE(m.seat_label, '') <> COALESCE(sp.seat_label, '')
|
||||
UNION ALL
|
||||
SELECT 'auth_users', COUNT(*)::text FROM auth.users
|
||||
ORDER BY table_name;
|
||||
SQL
|
||||
if [[ "${SCOPE}" == "analysis" || "${SCOPE}" == "full" ]]; then
|
||||
cat <<'SQL'
|
||||
SELECT 'integration_work_logs', COUNT(*)::text FROM public.integration_work_logs
|
||||
UNION ALL
|
||||
SELECT 'integration_vouchers', COUNT(*)::text FROM public.integration_vouchers
|
||||
ORDER BY 1;
|
||||
SQL
|
||||
fi
|
||||
} > "${VERIFY_SQL}"
|
||||
|
||||
echo "[8/8] Restarting 8081 app services and printing verification snapshot"
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" up -d frontend proxy >/dev/null
|
||||
run_compose "${DEV_DIR}" "${DEV_COMPOSE[@]}" exec -T db \
|
||||
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" -f - < "${VERIFY_SQL}"
|
||||
|
||||
echo
|
||||
echo "Sync complete."
|
||||
echo "Source: ${PROD_DIR} (8080)"
|
||||
echo "Target: ${DEV_DIR} (8081)"
|
||||
echo "Dev compose: ${DEV_COMPOSE_FILE}"
|
||||
echo "Dev project: ${DEV_PROJECT_NAME}"
|
||||
echo "Scope : ${SCOPE}"
|
||||
@@ -1,147 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MH 조직현황 대시보드 Standalone</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="http://localhost:8080/legacy/static/common.css">
|
||||
<link rel="stylesheet" href="http://localhost:8080/styles.css?v=20260326-standalone">
|
||||
</head>
|
||||
<body>
|
||||
<section id="login-panel" class="login-screen">
|
||||
<div class="login-backdrop">
|
||||
<form id="login-form" class="login-card">
|
||||
<div class="login-brand">
|
||||
<p class="eyebrow">GPD/TDC</p>
|
||||
<h1>MH Dash Board</h1>
|
||||
</div>
|
||||
|
||||
<div class="login-form-wrap">
|
||||
<label>
|
||||
<span>사번</span>
|
||||
<input name="username" type="text" placeholder="사번 입력" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>비번</span>
|
||||
<input name="password" type="password" placeholder="비밀번호 입력" required>
|
||||
</label>
|
||||
<button type="submit">로그인</button>
|
||||
<p id="login-message" class="helper-text"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="dashboard-panel" class="dashboard-shell hidden">
|
||||
<header class="dashboard-header">
|
||||
<div class="brand-block">
|
||||
<p class="eyebrow">MH Dashboard</p>
|
||||
<h2 id="current-view-title">조직 현황</h2>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<button class="nav-pill" type="button" data-view="ledger">사업관리대장</button>
|
||||
<button class="nav-pill" type="button" data-view="project">프로젝트별 분석</button>
|
||||
<button class="nav-pill" type="button" data-view="team">팀/개인별 분석</button>
|
||||
<button class="nav-pill active" type="button" data-view="organization">조직 현황</button>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<button id="user-badge" class="ghost-button ghost-button-soft user-chip" type="button"></button>
|
||||
<div id="user-popover" class="user-popover hidden"></div>
|
||||
<button id="logout-btn" class="ghost-button icon-button" type="button" title="로그아웃" aria-label="로그아웃">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="dashboard-main">
|
||||
<section id="organization-stage" class="main-stage">
|
||||
<div class="stage-frame">
|
||||
<iframe
|
||||
id="organization-frame"
|
||||
src="http://localhost:8080/legacy/organization?v=20260326-standalone"
|
||||
data-src="http://localhost:8080/legacy/organization?v=20260326-standalone"
|
||||
title="조직도 메인 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="seatmap-stage" class="main-stage" hidden>
|
||||
<div class="seatmap-layout">
|
||||
<div class="seatmap-topbar">
|
||||
<div>
|
||||
<p class="eyebrow">Seat Layout</p>
|
||||
<h3 id="seatmap-name">자리배치도</h3>
|
||||
</div>
|
||||
<div class="seatmap-actions">
|
||||
<button id="seatmap-save-btn" class="ghost-button" type="button" hidden disabled>저장</button>
|
||||
<button id="seatmap-cancel-btn" class="ghost-button ghost-button-soft" type="button" hidden>취소</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="seatmap-status" class="seatmap-status" role="status"></p>
|
||||
|
||||
<div class="seatmap-content">
|
||||
<div class="seatmap-board-panel">
|
||||
<div id="seatmap-empty" class="seatmap-empty hidden"></div>
|
||||
<div id="seatmap-board-wrap" class="seatmap-board-wrap hidden">
|
||||
<div id="seatmap-board" class="seatmap-board"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="seatmap-sidebar">
|
||||
<section id="seatmap-settings-panel" class="seatmap-panel hidden">
|
||||
<div class="seatmap-panel-head">
|
||||
<h4>배치도 설정</h4>
|
||||
<p>DXF 파일의 chair 레이어를 좌석 위치로 사용합니다.</p>
|
||||
</div>
|
||||
<form id="seatmap-settings-form" class="seatmap-form">
|
||||
<label>
|
||||
<span>배치도 이름</span>
|
||||
<input id="seatmap-form-name" name="name" type="text" placeholder="예: 본사 3층" required>
|
||||
</label>
|
||||
<div>
|
||||
<span>DXF 파일</span>
|
||||
<label class="seatmap-file-input" for="seatmap-form-image">
|
||||
<input id="seatmap-form-image" name="image" type="file" accept=".dxf" required>
|
||||
<span class="seatmap-file-button">DXF 선택</span>
|
||||
<strong id="seatmap-file-name" class="seatmap-file-name">선택된 파일 없음</strong>
|
||||
</label>
|
||||
</div>
|
||||
<button id="seatmap-settings-submit" type="submit">DXF 업로드</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="seatmap-panel">
|
||||
<div class="seatmap-panel-head">
|
||||
<h4>미배치 인원</h4>
|
||||
<p>이름을 검색하고 자리배치도에 바로 드래그하세요.</p>
|
||||
</div>
|
||||
<label class="seatmap-search">
|
||||
<span class="hidden">구성원 검색</span>
|
||||
<input id="seatmap-search" type="search" placeholder="이름 또는 부서 검색">
|
||||
</label>
|
||||
<div id="seatmap-unassigned" class="seatmap-member-list"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="empty-stage" class="main-stage" hidden>
|
||||
<div class="stage-empty"></div>
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
window.__MH_BASE_URL = "http://localhost:8080";
|
||||
</script>
|
||||
<script src="http://localhost:8080/app.js?v=20260326-standalone"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user