update webbase design
This commit is contained in:
116
CLAUDE.md
Normal file
116
CLAUDE.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Korean Enterprise GroupWare system built with C# .NET Framework 4.6 and Windows Forms. The application serves as a comprehensive business management system that includes project management, purchasing, attendance tracking, reporting, and web-based functionality. The project runs on port 7979 (previously 9000) and includes both desktop and web components.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Main Application (Project/EETGW.csproj)
|
||||
- **Entry Point**: `Project/Program.cs` - Handles WebView2Runtime extraction and starts the main form
|
||||
- **Main Form**: `Project/fMain.cs` - Primary application window
|
||||
- **Web Server**: Integrated OWIN-based web server for HTTP API and static files
|
||||
- **Database**: Microsoft SQL Server with Entity Framework 6.2.0
|
||||
- **Target Framework**: .NET Framework 4.6
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Web Layer** (`Project/Web/`):
|
||||
- **Startup.cs**: OWIN configuration for HTTP API and static file serving
|
||||
- **Controllers**: API controllers for various business functions (Home, Project, Purchase, Item, etc.)
|
||||
- **wwwroot**: Static web assets (HTML, CSS, JS files)
|
||||
|
||||
2. **SubProjects**: Modular components with specific business functionality:
|
||||
- **FPJ0000**: Project management module
|
||||
- **FCM0000**: Customer management
|
||||
- **FEQ0000**: Equipment management
|
||||
- **FBS0000**: Holiday/attendance management
|
||||
- **FCOMMON**: Shared common functionality
|
||||
- **WebServer**: Additional web services
|
||||
- **AmkorRestfulService**: REST API services
|
||||
|
||||
3. **Sub Components** (`Sub/`):
|
||||
- **arCtl**: Custom controls library
|
||||
- **arftp**: FTP functionality
|
||||
- **tcpservice**: TCP communication services
|
||||
- **YARTE**: HTML editor component
|
||||
- **StaffLayoutCtl**: Staff layout controls
|
||||
|
||||
### Technology Stack
|
||||
- **UI Framework**: Windows Forms with custom controls (FarPoint Spread grids)
|
||||
- **Web Framework**: OWIN with ASP.NET Web API 5.2.9
|
||||
- **Database ORM**: Entity Framework 6.2.0
|
||||
- **JSON Processing**: Newtonsoft.Json 13.0.3
|
||||
- **Web Browser**: Microsoft WebView2 1.0.2210.55
|
||||
- **Reports**: Microsoft ReportViewer 15.0
|
||||
- **Excel Processing**: libxl.net and CsvHelper 30.0.1
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Building the Solution
|
||||
```bash
|
||||
# Build the entire solution
|
||||
msbuild EETGW.sln /p:Configuration=Debug /p:Platform="Any CPU"
|
||||
|
||||
# Build for release
|
||||
msbuild EETGW.sln /p:Configuration=Release /p:Platform="Any CPU"
|
||||
|
||||
# Build specific project
|
||||
msbuild Project/EETGW.csproj /p:Configuration=Debug
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
- **Debug Mode**: Run from Visual Studio or build and execute the output from `Project/bin/Debug/`
|
||||
- **Web Server**: Automatically starts on port 7979 when the application launches
|
||||
- **Database**: Ensure SQL Server connection string is configured in app.config
|
||||
|
||||
### Package Management
|
||||
- Uses NuGet packages defined in `packages.config` files throughout the solution
|
||||
- Restore packages using: `nuget restore EETGW.sln`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Database Connection
|
||||
- Connection strings configured in individual `app.config` files
|
||||
- Primary database connection in `Project/app.config`
|
||||
- Uses Entity Framework with SQL Server
|
||||
|
||||
### Web Server Configuration
|
||||
- **Port**: 7979 (configured in startup)
|
||||
- **Static Files**: Served from `Project/Web/wwwroot/`
|
||||
- **API Routes**: Configured in `Project/Web/Startup.cs`
|
||||
- **CORS**: Enabled for all origins
|
||||
|
||||
### Build Configurations
|
||||
- **Debug**: Outputs to `Project/bin/Debug/` with x86 platform target
|
||||
- **Release**: Optimized build configuration
|
||||
- Different output paths for various configurations (see EETGW.csproj)
|
||||
|
||||
## Key Conventions
|
||||
|
||||
### Code Organization
|
||||
- Korean comments and variable names are common throughout the codebase
|
||||
- Business logic separated into modular SubProjects
|
||||
- Shared functionality centralized in FCOMMON project
|
||||
- Custom controls and utilities in Sub/ directory
|
||||
|
||||
### File Structure
|
||||
- Each SubProject has its own namespace and assembly
|
||||
- Form files follow naming convention: `f[FormName].cs` with corresponding `.Designer.cs` and `.resx`
|
||||
- Dataset files use `.xsd` schemas with generated code
|
||||
|
||||
### Dependencies
|
||||
- Heavy use of FarPoint Spread controls for data grids
|
||||
- Custom logging via ArLog.Net4.dll
|
||||
- Settings management through ArSetting.Net4.dll
|
||||
- Multiple third-party libraries for Excel, FTP, and web functionality
|
||||
|
||||
## Development Notes
|
||||
|
||||
- WebView2Runtime is automatically extracted on first run from WebView2Runtime.zip
|
||||
- The application includes comprehensive error handling and logging
|
||||
- Multiple authentication methods including AD integration
|
||||
- Supports both Korean and English localization
|
||||
- Includes extensive reporting capabilities with RDLC files
|
||||
@@ -22,7 +22,8 @@ namespace Project.Dialog
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
|
||||
this.Text = "공용코드 관리";
|
||||
this.Size = new Size(1000, 900);
|
||||
InitializeWebView2();
|
||||
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System">
|
||||
<HintPath>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.dll</HintPath>
|
||||
<HintPath>..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Buffers.4.5.1\lib\netstandard1.1\System.Buffers.dll</HintPath>
|
||||
@@ -670,22 +670,25 @@
|
||||
<Content Include="SqlServerTypes\x86\SqlServerSpatial140.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Web\wwwroot\DashBoard\index.html">
|
||||
<None Include="Web\wwwroot\DashBoard\index.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</None>
|
||||
<None Include="Web\wwwroot\vnc-server.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="Web\wwwroot\Common.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="Web\wwwroot\Jobreport\index.html">
|
||||
<None Include="Web\wwwroot\Jobreport\index.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Web\wwwroot\Kuntae\index.html">
|
||||
</None>
|
||||
<None Include="Web\wwwroot\js\common-nav.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Web\wwwroot\login.html">
|
||||
</None>
|
||||
<None Include="Web\wwwroot\Kuntae\index.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="Web\wwwroot\index.html">
|
||||
</None>
|
||||
<None Include="Web\wwwroot\login.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
@@ -770,9 +773,7 @@
|
||||
<ItemGroup>
|
||||
<WCFMetadata Include="Connected Services\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Web\wwwroot\js\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\packages\Microsoft.Web.WebView2.1.0.2210.55\build\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.2210.55\build\Microsoft.Web.WebView2.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
|
||||
@@ -15,20 +15,34 @@ namespace Project.Web.Controllers
|
||||
public HttpResponseMessage GetList(string grp=null)
|
||||
{
|
||||
var sql = string.Empty;
|
||||
sql = "select *" +
|
||||
" from common" +
|
||||
" where gcode = @gcode" +
|
||||
" and grp = @grp" +
|
||||
" order by code,svalue";
|
||||
|
||||
var cs = Properties.Settings.Default.gwcs;// "Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D!";
|
||||
if (string.IsNullOrEmpty(grp))
|
||||
{
|
||||
// grp가 없으면 모든 그룹의 데이터를 가져옴
|
||||
sql = "select *" +
|
||||
" from common" +
|
||||
" where gcode = @gcode" +
|
||||
" order by grp, code, svalue";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 특정 그룹의 데이터만 가져옴
|
||||
sql = "select *" +
|
||||
" from common" +
|
||||
" where gcode = @gcode" +
|
||||
" and grp = @grp" +
|
||||
" order by code,svalue";
|
||||
}
|
||||
|
||||
var cs = Properties.Settings.Default.gwcs;
|
||||
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||
|
||||
// 날짜 파라미터가 없으면 기본값 사용 (현재 월)
|
||||
var grpCode = !string.IsNullOrEmpty(grp) ? grp : "99";
|
||||
cmd.Parameters.AddWithValue("grp", grpCode);
|
||||
if (!string.IsNullOrEmpty(grp))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("grp", grp);
|
||||
}
|
||||
|
||||
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||
var dt = new System.Data.DataTable();
|
||||
@@ -83,7 +97,262 @@ namespace Project.Web.Controllers
|
||||
return resp;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public HttpResponseMessage Save([FromBody] CommonModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = Properties.Settings.Default.gwcs;
|
||||
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||
var sql = string.Empty;
|
||||
var cmd = new System.Data.SqlClient.SqlCommand();
|
||||
cmd.Connection = cn;
|
||||
|
||||
if (model.idx > 0)
|
||||
{
|
||||
// 업데이트
|
||||
sql = @"UPDATE common SET
|
||||
grp = @grp,
|
||||
code = @code,
|
||||
svalue = @svalue,
|
||||
ivalue = @ivalue,
|
||||
fvalue = @fvalue,
|
||||
svalue2 = @svalue2,
|
||||
memo = @memo,
|
||||
wuid = @wuid,
|
||||
wdate = GETDATE()
|
||||
WHERE idx = @idx AND gcode = @gcode";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 신규 추가
|
||||
sql = @"INSERT INTO common (gcode, grp, code, svalue, ivalue, fvalue, svalue2, memo, wuid, wdate)
|
||||
VALUES (@gcode, @grp, @code, @svalue, @ivalue, @fvalue, @svalue2, @memo, @wuid, GETDATE())";
|
||||
}
|
||||
|
||||
cmd.CommandText = sql;
|
||||
cmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
cmd.Parameters.AddWithValue("@grp", model.grp ?? "");
|
||||
cmd.Parameters.AddWithValue("@code", model.code ?? "");
|
||||
cmd.Parameters.AddWithValue("@svalue", model.svalue ?? "");
|
||||
cmd.Parameters.AddWithValue("@ivalue", model.ivalue);
|
||||
cmd.Parameters.AddWithValue("@fvalue", model.fvalue);
|
||||
cmd.Parameters.AddWithValue("@svalue2", model.svalue2 ?? "");
|
||||
cmd.Parameters.AddWithValue("@memo", model.memo ?? "");
|
||||
cmd.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||
|
||||
if (model.idx > 0)
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@idx", model.idx);
|
||||
}
|
||||
|
||||
cn.Open();
|
||||
var result = cmd.ExecuteNonQuery();
|
||||
cn.Close();
|
||||
|
||||
cmd.Dispose();
|
||||
cn.Dispose();
|
||||
|
||||
var response = new
|
||||
{
|
||||
Success = result > 0,
|
||||
Message = result > 0 ? "저장되었습니다." : "저장에 실패했습니다."
|
||||
};
|
||||
|
||||
return CreateJsonResponse(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
Success = false,
|
||||
Message = "오류가 발생했습니다: " + ex.Message
|
||||
};
|
||||
return CreateJsonResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public HttpResponseMessage Delete([FromBody] DeleteModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = Properties.Settings.Default.gwcs;
|
||||
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||
var sql = "DELETE FROM common WHERE idx = @idx AND gcode = @gcode";
|
||||
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||
|
||||
cmd.Parameters.AddWithValue("@idx", model.idx);
|
||||
cmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
|
||||
cn.Open();
|
||||
var result = cmd.ExecuteNonQuery();
|
||||
cn.Close();
|
||||
|
||||
cmd.Dispose();
|
||||
cn.Dispose();
|
||||
|
||||
var response = new
|
||||
{
|
||||
Success = result > 0,
|
||||
Message = result > 0 ? "삭제되었습니다." : "삭제에 실패했습니다."
|
||||
};
|
||||
|
||||
return CreateJsonResponse(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
Success = false,
|
||||
Message = "오류가 발생했습니다: " + ex.Message
|
||||
};
|
||||
return CreateJsonResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public HttpResponseMessage GetGroups()
|
||||
{
|
||||
try
|
||||
{
|
||||
var sql = "select code, svalue from common WITH (nolock) " +
|
||||
"where gcode = @gcode and grp = '99' " +
|
||||
"order by code";
|
||||
|
||||
var cs = Properties.Settings.Default.gwcs;
|
||||
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||
cmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
|
||||
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||
var dt = new System.Data.DataTable();
|
||||
da.Fill(dt);
|
||||
da.Dispose();
|
||||
cmd.Dispose();
|
||||
cn.Dispose();
|
||||
|
||||
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
|
||||
var resp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
txtjson,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
|
||||
return resp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
Message = ex.Message,
|
||||
};
|
||||
return CreateJsonResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public HttpResponseMessage InitializeGroups()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = Properties.Settings.Default.gwcs;
|
||||
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||
|
||||
// 기본 그룹코드들 정의
|
||||
var defaultGroups = new[]
|
||||
{
|
||||
new { code = "01", svalue = "부서코드" },
|
||||
new { code = "02", svalue = "직급코드" },
|
||||
new { code = "03", svalue = "공정코드" },
|
||||
new { code = "04", svalue = "품목분류" },
|
||||
new { code = "05", svalue = "업체분류" },
|
||||
new { code = "06", svalue = "제조공정" },
|
||||
new { code = "07", svalue = "장비제조" },
|
||||
new { code = "08", svalue = "장비모델" },
|
||||
new { code = "09", svalue = "장비기술" },
|
||||
new { code = "99", svalue = "기타" }
|
||||
};
|
||||
|
||||
cn.Open();
|
||||
|
||||
int insertedCount = 0;
|
||||
foreach (var group in defaultGroups)
|
||||
{
|
||||
// 이미 존재하는지 확인
|
||||
var checkSql = "SELECT COUNT(*) FROM common WHERE gcode = @gcode AND grp = '99' AND code = @code";
|
||||
var checkCmd = new System.Data.SqlClient.SqlCommand(checkSql, cn);
|
||||
checkCmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
checkCmd.Parameters.AddWithValue("@code", group.code);
|
||||
|
||||
var exists = (int)checkCmd.ExecuteScalar() > 0;
|
||||
checkCmd.Dispose();
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
// 새로 추가
|
||||
var insertSql = @"INSERT INTO common (gcode, grp, code, svalue, ivalue, fvalue, svalue2, memo, wuid, wdate)
|
||||
VALUES (@gcode, '99', @code, @svalue, 0, 0.0, '', '코드그룹 정의', @wuid, GETDATE())";
|
||||
var insertCmd = new System.Data.SqlClient.SqlCommand(insertSql, cn);
|
||||
insertCmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
insertCmd.Parameters.AddWithValue("@code", group.code);
|
||||
insertCmd.Parameters.AddWithValue("@svalue", group.svalue);
|
||||
insertCmd.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||
|
||||
insertCmd.ExecuteNonQuery();
|
||||
insertCmd.Dispose();
|
||||
insertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
cn.Close();
|
||||
cn.Dispose();
|
||||
|
||||
var response = new
|
||||
{
|
||||
Success = true,
|
||||
Message = $"그룹코드 초기화 완료. {insertedCount}개 추가됨."
|
||||
};
|
||||
|
||||
return CreateJsonResponse(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
Success = false,
|
||||
Message = "오류가 발생했습니다: " + ex.Message
|
||||
};
|
||||
return CreateJsonResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpResponseMessage CreateJsonResponse(object data)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
|
||||
return new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
json,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteModel
|
||||
{
|
||||
public int idx { get; set; }
|
||||
}
|
||||
|
||||
public class CommonModel
|
||||
|
||||
@@ -322,8 +322,8 @@ namespace Project.Web.Controllers
|
||||
[HttpGet]
|
||||
public HttpResponseMessage Index()
|
||||
{
|
||||
// 직접 파일을 읽어서 반환
|
||||
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "index.html");
|
||||
// DashBoard로 리디렉션하도록 변경
|
||||
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "DashBoard", "index.html");
|
||||
var contents = string.Empty;
|
||||
|
||||
if (System.IO.File.Exists(filePath))
|
||||
|
||||
@@ -22,6 +22,13 @@ namespace Project.OWIN
|
||||
//라우팅 설정
|
||||
config.MapHttpAttributeRoutes();
|
||||
|
||||
// 컨트롤러만 있는 경우 기본 액션을 Index로 설정
|
||||
config.Routes.MapHttpRoute(
|
||||
name: "ControllerOnly",
|
||||
routeTemplate: "{controller}",
|
||||
defaults: new { action = "Index" }
|
||||
);
|
||||
|
||||
config.Routes.MapHttpRoute(
|
||||
name: "DefaultApi",
|
||||
routeTemplate: "{controller}/{action}/{id}",
|
||||
|
||||
@@ -23,8 +23,694 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일링 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 애니메이션 */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(10px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 셀렉트 박스 옵션 스타일링 */
|
||||
select option {
|
||||
background-color: #374151 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
select option:hover {
|
||||
background-color: #4B5563 !important;
|
||||
}
|
||||
|
||||
select option:checked {
|
||||
background-color: #6366F1 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
intro file
|
||||
<body class="gradient-bg min-h-screen">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 헤더 -->
|
||||
<div class="text-center mb-8 animate-fade-in">
|
||||
<h1 class="text-4xl font-bold text-white mb-2">공용코드 관리</h1>
|
||||
<p class="text-white/80 text-lg">시스템 공용코드를 관리합니다</p>
|
||||
</div>
|
||||
|
||||
<!-- 개발중 경고 메시지 -->
|
||||
<div class="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
||||
<p class="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 검색 및 필터 -->
|
||||
<div class="glass-effect rounded-2xl p-6 mb-6 card-hover animate-slide-up">
|
||||
<div class="flex flex-wrap gap-4 items-end">
|
||||
<div class="flex-1 min-w-0">
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">코드그룹</label>
|
||||
<select id="grpFilter" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
||||
<!-- 동적으로 로드됩니다 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="loadData()" class="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-6 py-2 rounded-lg transition-all border border-white/30 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
조회
|
||||
</button>
|
||||
<button onclick="showAddModal()" class="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-6 py-2 rounded-lg transition-all border border-white/30 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 데이터 테이블 -->
|
||||
<div class="glass-effect rounded-2xl overflow-hidden card-hover animate-slide-up">
|
||||
<div class="overflow-x-auto max-h-[calc(100vh-300px)] overflow-y-auto custom-scrollbar">
|
||||
<table class="w-full">
|
||||
<thead class="bg-white/10 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">코드</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">비고</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(문자열)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(숫자)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(실수)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값2</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dataTable" class="divide-y divide-white/10">
|
||||
<!-- 데이터가 여기에 표시됩니다 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="px-6 py-3 border-t border-white/10 bg-white/5">
|
||||
<p class="text-white/70 text-sm text-center">총 <span id="recordCount" class="text-white font-medium">0</span>건</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 로딩 인디케이터 -->
|
||||
<div id="loadingIndicator" class="fixed top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2 text-white text-sm hidden">
|
||||
<div class="flex items-center">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
데이터 로딩 중...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 추가/편집 모달 -->
|
||||
<div id="editModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up">
|
||||
<!-- 모달 헤더 -->
|
||||
<div class="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h2 id="modalTitle" class="text-xl font-semibold text-white">공용코드 추가</h2>
|
||||
<button onclick="hideEditModal()" class="text-white/70 hover:text-white transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 모달 내용 -->
|
||||
<form id="editForm" class="p-6">
|
||||
<input type="hidden" id="editIdx" value="">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">코드그룹 *</label>
|
||||
<select id="editGrp" required class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
||||
<option value="" class="bg-gray-800 text-white">선택하세요</option>
|
||||
<!-- 동적으로 로드됩니다 -->
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">코드 *</label>
|
||||
<input type="text" id="editCode" required class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="코드를 입력하세요">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">값(문자열)</label>
|
||||
<input type="text" id="editSvalue" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="문자열 값">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">값(숫자)</label>
|
||||
<input type="number" id="editIvalue" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="숫자 값">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">값(실수)</label>
|
||||
<input type="number" step="0.01" id="editFvalue" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="실수 값">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">값2</label>
|
||||
<input type="text" id="editSvalue2" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="추가 문자열 값">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-white/70 mb-2">비고</label>
|
||||
<textarea id="editMemo" rows="3" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="비고사항을 입력하세요"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 모달 푸터 -->
|
||||
<div class="px-6 py-4 border-t border-white/10 flex justify-end gap-2">
|
||||
<button onclick="hideEditModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
취소
|
||||
</button>
|
||||
<button onclick="saveData()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 삭제 확인 모달 -->
|
||||
<div id="deleteModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="glass-effect rounded-2xl w-full max-w-md animate-slide-up">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">삭제 확인</h3>
|
||||
<p class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 mb-6">선택한 공용코드를 삭제하시겠습니까?</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button onclick="hideDeleteModal()" class="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-lg transition-colors">
|
||||
취소
|
||||
</button>
|
||||
<button onclick="confirmDelete()" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 공통 네비게이션 컴포넌트
|
||||
class CommonNavigation {
|
||||
constructor(currentPage = '') {
|
||||
this.currentPage = currentPage;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createNavigation();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
createNavigation() {
|
||||
const nav = document.createElement('nav');
|
||||
nav.className = 'glass-effect border-b border-white/10';
|
||||
nav.innerHTML = this.getNavigationHTML();
|
||||
|
||||
// body의 첫 번째 자식으로 추가
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
}
|
||||
|
||||
getNavigationHTML() {
|
||||
return `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 로고/타이틀 -->
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-xl font-bold text-white">GroupWare</h2>
|
||||
</div>
|
||||
|
||||
<!-- 메뉴 -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 버튼 -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 -->
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
|
||||
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
getMobileMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
// 모바일 메뉴 토글
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 내비게이션 초기화
|
||||
function initNavigation(currentPage = '') {
|
||||
// DOM이 로드된 후에 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new CommonNavigation(currentPage);
|
||||
});
|
||||
} else {
|
||||
new CommonNavigation(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
let currentData = [];
|
||||
let deleteTargetIdx = null;
|
||||
let groupData = [];
|
||||
|
||||
// 페이지 로드시 초기 데이터 로드
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadGroups();
|
||||
});
|
||||
|
||||
// 코드그룹 목록 로드
|
||||
function loadGroups() {
|
||||
showLoading();
|
||||
|
||||
fetch('http://127.0.0.1:7979/Common/GetGroups')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
groupData = data || [];
|
||||
renderGroupSelects();
|
||||
|
||||
// 기본적으로 "99-전체" 선택
|
||||
document.getElementById('grpFilter').value = '99';
|
||||
loadData();
|
||||
hideLoading();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('그룹 데이터 로드 중 오류 발생:', error);
|
||||
hideLoading();
|
||||
showNotification('그룹 데이터 로드 중 오류가 발생했습니다.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 셀렉트 박스들에 그룹 옵션 렌더링
|
||||
function renderGroupSelects() {
|
||||
const grpFilter = document.getElementById('grpFilter');
|
||||
const editGrp = document.getElementById('editGrp');
|
||||
|
||||
// 필터 셀렉트박스 - "99-전체" 옵션 추가
|
||||
const filterOptions = [
|
||||
'<option value="99" class="bg-gray-800 text-white">99-전체</option>'
|
||||
].concat(
|
||||
groupData.map(group =>
|
||||
`<option value="${group.code}" class="bg-gray-800 text-white">${group.code}-${group.svalue}</option>`
|
||||
)
|
||||
);
|
||||
grpFilter.innerHTML = filterOptions.join('');
|
||||
|
||||
// 편집 모달 셀렉트박스
|
||||
editGrp.innerHTML = '<option value="" class="bg-gray-800 text-white">선택하세요</option>' +
|
||||
groupData.map(group =>
|
||||
`<option value="${group.code}" class="bg-gray-800 text-white">${group.code}-${group.svalue}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
// 데이터 로드
|
||||
function loadData() {
|
||||
const grp = document.getElementById('grpFilter').value;
|
||||
showLoading();
|
||||
|
||||
let url = 'http://127.0.0.1:7979/Common/GetList';
|
||||
if (grp && grp !== '99') {
|
||||
url += '?grp=' + encodeURIComponent(grp);
|
||||
}
|
||||
// grp가 '99'이면 모든 그룹의 데이터를 가져옴 (파라미터 없음)
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
currentData = data || [];
|
||||
renderTable();
|
||||
hideLoading();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('데이터 로드 중 오류 발생:', error);
|
||||
hideLoading();
|
||||
showNotification('데이터 로드 중 오류가 발생했습니다.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 테이블 렌더링
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('dataTable');
|
||||
const recordCount = document.getElementById('recordCount');
|
||||
const selectedGrp = document.getElementById('grpFilter').value;
|
||||
const showAllGroups = selectedGrp === '99';
|
||||
|
||||
if (currentData.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-4 text-center text-white/70">데이터가 없습니다.</td></tr>';
|
||||
recordCount.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
recordCount.textContent = currentData.length;
|
||||
|
||||
tbody.innerHTML = currentData.map(item => {
|
||||
// 전체 보기일 때는 코드에 그룹 정보도 표시
|
||||
let codeDisplay = item.code || '-';
|
||||
if (showAllGroups && item.grp) {
|
||||
// 그룹 데이터에서 해당 그룹의 이름 찾기
|
||||
const groupInfo = groupData.find(g => g.code === item.grp);
|
||||
const groupName = groupInfo ? groupInfo.svalue : item.grp;
|
||||
codeDisplay = `${item.grp}-${groupName}`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${codeDisplay}</td>
|
||||
<td class="px-6 py-4 text-sm text-white">${item.memo || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.svalue || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.ivalue || '0'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.fvalue || '0.0'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.svalue2 || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button onclick="editItem(${item.idx})" class="text-blue-400 hover:text-blue-300 mr-2 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="deleteItem(${item.idx})" class="text-red-400 hover:text-red-300 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 추가 모달 표시
|
||||
function showAddModal() {
|
||||
document.getElementById('modalTitle').textContent = '공용코드 추가';
|
||||
document.getElementById('editForm').reset();
|
||||
document.getElementById('editIdx').value = '';
|
||||
|
||||
// 현재 선택된 코드그룹을 자동 입력
|
||||
const selectedGrp = document.getElementById('grpFilter').value;
|
||||
document.getElementById('editGrp').value = selectedGrp;
|
||||
|
||||
document.getElementById('editModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 편집 모달 표시
|
||||
function editItem(idx) {
|
||||
const item = currentData.find(x => x.idx === idx);
|
||||
if (!item) return;
|
||||
|
||||
document.getElementById('modalTitle').textContent = '공용코드 편집';
|
||||
document.getElementById('editIdx').value = item.idx;
|
||||
document.getElementById('editGrp').value = item.grp || '';
|
||||
document.getElementById('editCode').value = item.code || '';
|
||||
document.getElementById('editSvalue').value = item.svalue || '';
|
||||
document.getElementById('editIvalue').value = item.ivalue || '';
|
||||
document.getElementById('editFvalue').value = item.fvalue || '';
|
||||
document.getElementById('editSvalue2').value = item.svalue2 || '';
|
||||
document.getElementById('editMemo').value = item.memo || '';
|
||||
|
||||
document.getElementById('editModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 편집 모달 숨기기
|
||||
function hideEditModal() {
|
||||
document.getElementById('editModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 삭제 확인
|
||||
function deleteItem(idx) {
|
||||
deleteTargetIdx = idx;
|
||||
document.getElementById('deleteModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 삭제 모달 숨기기
|
||||
function hideDeleteModal() {
|
||||
document.getElementById('deleteModal').classList.add('hidden');
|
||||
deleteTargetIdx = null;
|
||||
}
|
||||
|
||||
// 삭제 실행
|
||||
function confirmDelete() {
|
||||
if (!deleteTargetIdx) return;
|
||||
|
||||
showLoading();
|
||||
|
||||
fetch('http://127.0.0.1:7979/Common/Delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ idx: deleteTargetIdx })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideLoading();
|
||||
if (data.Success) {
|
||||
showNotification(data.Message, 'success');
|
||||
hideDeleteModal();
|
||||
loadData(); // 데이터 새로고침
|
||||
} else {
|
||||
showNotification(data.Message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
console.error('삭제 중 오류 발생:', error);
|
||||
showNotification('삭제 중 오류가 발생했습니다.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 데이터 저장
|
||||
function saveData() {
|
||||
const form = document.getElementById('editForm');
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
idx: parseInt(document.getElementById('editIdx').value) || 0,
|
||||
grp: document.getElementById('editGrp').value,
|
||||
code: document.getElementById('editCode').value,
|
||||
svalue: document.getElementById('editSvalue').value,
|
||||
ivalue: parseInt(document.getElementById('editIvalue').value) || 0,
|
||||
fvalue: parseFloat(document.getElementById('editFvalue').value) || 0.0,
|
||||
svalue2: document.getElementById('editSvalue2').value,
|
||||
memo: document.getElementById('editMemo').value
|
||||
};
|
||||
|
||||
showLoading();
|
||||
|
||||
fetch('http://127.0.0.1:7979/Common/Save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
hideLoading();
|
||||
if (result.Success) {
|
||||
showNotification(result.Message, 'success');
|
||||
hideEditModal();
|
||||
loadData(); // 데이터 새로고침
|
||||
} else {
|
||||
showNotification(result.Message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
console.error('저장 중 오류 발생:', error);
|
||||
showNotification('저장 중 오류가 발생했습니다.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 로딩 표시
|
||||
function showLoading() {
|
||||
document.getElementById('loadingIndicator').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 로딩 숨기기
|
||||
function hideLoading() {
|
||||
document.getElementById('loadingIndicator').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 알림 표시
|
||||
function showNotification(message, type = 'info') {
|
||||
const colors = {
|
||||
info: 'bg-blue-500/90 backdrop-blur-sm',
|
||||
success: 'bg-green-500/90 backdrop-blur-sm',
|
||||
warning: 'bg-yellow-500/90 backdrop-blur-sm',
|
||||
error: 'bg-red-500/90 backdrop-blur-sm'
|
||||
};
|
||||
|
||||
const icons = {
|
||||
info: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>`,
|
||||
success: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>`,
|
||||
warning: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>`,
|
||||
error: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`;
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
${icons[type]}
|
||||
<span class="ml-2">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 초기 상태 설정 (오른쪽에서 슬라이드 인)
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
notification.style.opacity = '0';
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 애니메이션으로 표시
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
notification.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// 자동 숨김
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 키보드 이벤트
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
hideEditModal();
|
||||
hideDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 공통 네비게이션 초기화
|
||||
initNavigation('common');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,6 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<meta name="version" content="v2.0-20250127">
|
||||
<title>근태현황 대시보드</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
@@ -458,6 +462,118 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 공통 네비게이션 컴포넌트
|
||||
class CommonNavigation {
|
||||
constructor(currentPage = '') {
|
||||
this.currentPage = currentPage;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createNavigation();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
createNavigation() {
|
||||
const nav = document.createElement('nav');
|
||||
nav.className = 'glass-effect border-b border-white/10';
|
||||
nav.innerHTML = this.getNavigationHTML();
|
||||
|
||||
// body의 첫 번째 자식으로 추가
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
}
|
||||
|
||||
getNavigationHTML() {
|
||||
return `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 로고/타이틀 -->
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-xl font-bold text-white">GroupWare</h2>
|
||||
</div>
|
||||
|
||||
<!-- 메뉴 -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 버튼 -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 -->
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
|
||||
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
getMobileMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
// 모바일 메뉴 토글
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 내비게이션 초기화
|
||||
function initNavigation(currentPage = '') {
|
||||
// DOM이 로드된 후에 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new CommonNavigation(currentPage);
|
||||
});
|
||||
} else {
|
||||
new CommonNavigation(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
// 휴가 인원 Ajax 업데이트
|
||||
function updateLeaveCount() {
|
||||
showLoading();
|
||||
@@ -652,6 +768,9 @@
|
||||
updateCurrentUserCount();
|
||||
}, 30000);
|
||||
|
||||
// 공통 네비게이션 초기화
|
||||
initNavigation('dashboard');
|
||||
|
||||
// 출근 대상자 모달 표시
|
||||
function showPresentUserModal() {
|
||||
document.getElementById('presentUserModal').classList.remove('hidden');
|
||||
|
||||
@@ -15,105 +15,207 @@
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3B82F6',
|
||||
secondary: '#6B7280',
|
||||
success: '#10B981',
|
||||
danger: '#EF4444',
|
||||
warning: '#F59E0B'
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
success: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
},
|
||||
warning: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f',
|
||||
},
|
||||
danger: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일링 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: var(--scrollbar-width, 16px);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 헤더 -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center py-4">
|
||||
<div class="flex items-center">
|
||||
<i data-feather="file-text" class="w-8 h-8 text-primary mr-3"></i>
|
||||
<h1 class="text-2xl font-bold text-gray-900">업무일지</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="refreshBtn" class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center">
|
||||
<i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
|
||||
새로고침
|
||||
</button>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span id="currentDate"></span>
|
||||
</div>
|
||||
<body class="gradient-bg min-h-screen">
|
||||
<!-- 네비게이션 메뉴 (동적으로 추가됨) -->
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 헤더 -->
|
||||
<div class="text-center mb-8 animate-fade-in">
|
||||
<h1 class="text-4xl font-bold text-white mb-2">업무일지</h1>
|
||||
<div class="flex justify-center items-center space-x-4">
|
||||
<button id="refreshBtn" class="glass-effect text-white px-4 py-2 rounded-lg flex items-center hover:bg-white/30 transition-colors">
|
||||
<i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
|
||||
새로고침
|
||||
</button>
|
||||
<div class="text-white/80">
|
||||
<span id="currentDate"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 개발중 경고 메시지 -->
|
||||
<div class="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
||||
<p class="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 메인 컨텐츠 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 통계 카드 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 animate-slide-up">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-blue-100 rounded-lg">
|
||||
<i data-feather="calendar" class="w-6 h-6 text-primary"></i>
|
||||
<div class="p-3 bg-primary-500/20 rounded-lg">
|
||||
<i data-feather="calendar" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">총 업무일수</p>
|
||||
<p id="totalDays" class="text-2xl font-bold text-gray-900">0</p>
|
||||
<p class="text-sm font-medium text-white/80">총 업무일수</p>
|
||||
<p id="totalDays" class="text-2xl font-bold text-white">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-green-100 rounded-lg">
|
||||
<i data-feather="clock" class="w-6 h-6 text-success"></i>
|
||||
<div class="p-3 bg-success-500/20 rounded-lg">
|
||||
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">총 근무시간</p>
|
||||
<p id="totalHours" class="text-2xl font-bold text-gray-900">0h</p>
|
||||
<p class="text-sm font-medium text-white/80">총 근무시간</p>
|
||||
<p id="totalHours" class="text-2xl font-bold text-white">0h</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-orange-100 rounded-lg">
|
||||
<i data-feather="zap" class="w-6 h-6 text-warning"></i>
|
||||
<div class="p-3 bg-warning-500/20 rounded-lg">
|
||||
<i data-feather="zap" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">총 초과근무</p>
|
||||
<p id="totalOT" class="text-2xl font-bold text-gray-900">0h</p>
|
||||
<p class="text-sm font-medium text-white/80">총 초과근무</p>
|
||||
<p id="totalOT" class="text-2xl font-bold text-white">0h</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-purple-100 rounded-lg">
|
||||
<i data-feather="folder" class="w-6 h-6 text-purple-600"></i>
|
||||
<div class="p-3 bg-purple-500/20 rounded-lg">
|
||||
<i data-feather="folder" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">진행중 프로젝트</p>
|
||||
<p id="activeProjects" class="text-2xl font-bold text-gray-900">0</p>
|
||||
<p class="text-sm font-medium text-white/80">진행중 프로젝트</p>
|
||||
<p id="activeProjects" class="text-2xl font-bold text-white">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 및 검색 -->
|
||||
<div class="bg-white rounded-lg shadow mb-6">
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="glass-effect rounded-lg mb-6 animate-slide-up">
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
|
||||
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">조회기간</label>
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">조회기간</label>
|
||||
<div class="flex space-x-2">
|
||||
<input type="date" id="startDate" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm">
|
||||
<span class="flex items-center text-gray-500">~</span>
|
||||
<input type="date" id="endDate" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm">
|
||||
<input type="date" id="startDate" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent text-sm">
|
||||
<span class="flex items-center text-white/60">~</span>
|
||||
<input type="date" id="endDate" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
|
||||
<select id="statusFilter" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">상태</label>
|
||||
<select id="statusFilter" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
||||
<option value="">전체</option>
|
||||
<option value="진행중">진행중</option>
|
||||
<option value="완료">완료</option>
|
||||
@@ -121,22 +223,22 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
|
||||
<select id="projectFilter" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">프로젝트</label>
|
||||
<select id="projectFilter" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">검색</label>
|
||||
<input type="text" id="searchInput" placeholder="업무 내용 검색..." class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">검색</label>
|
||||
<input type="text" id="searchInput" placeholder="업무 내용 검색..." class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="clearFilterBtn" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-md flex items-center">
|
||||
<button id="clearFilterBtn" class="glass-effect hover:bg-white/30 text-white px-4 py-2 rounded-md flex items-center transition-colors">
|
||||
<i data-feather="x" class="w-4 h-4 mr-2"></i>
|
||||
필터 초기화
|
||||
</button>
|
||||
<button id="exportBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md flex items-center">
|
||||
<button id="exportBtn" class="glass-effect hover:bg-white/30 text-white px-4 py-2 rounded-md flex items-center transition-colors">
|
||||
<i data-feather="download" class="w-4 h-4 mr-2"></i>
|
||||
엑셀 다운로드
|
||||
</button>
|
||||
@@ -146,43 +248,43 @@
|
||||
</div>
|
||||
|
||||
<!-- 데이터 테이블 -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="glass-effect rounded-lg overflow-hidden animate-slide-up custom-scrollbar">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<table class="min-w-full divide-y divide-white/20">
|
||||
<thead class="bg-white/10">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="pdate">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="pdate">
|
||||
날짜 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="status">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="status">
|
||||
상태 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="projectName">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="projectName">
|
||||
프로젝트명 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="requestpart">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="requestpart">
|
||||
요청부서 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="package">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="package">
|
||||
패키지 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="type">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="type">
|
||||
타입 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="process">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="process">
|
||||
프로세스 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">업무내용</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="hrs">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">업무내용</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="hrs">
|
||||
근무시간 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="ot">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="ot">
|
||||
초과근무 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">초과근무 시간</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">초과근무 시간</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobTableBody" class="bg-white divide-y divide-gray-200">
|
||||
<tbody id="jobTableBody" class="divide-y divide-white/10">
|
||||
<!-- 데이터가 여기에 동적으로 로드됩니다 -->
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -190,27 +292,27 @@
|
||||
<!-- 로딩 상태 -->
|
||||
<div id="loadingState" class="hidden p-8 text-center">
|
||||
<div class="inline-flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-gray-600">데이터를 불러오는 중...</span>
|
||||
<span class="text-white/80">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 빈 상태 -->
|
||||
<div id="emptyState" class="hidden p-8 text-center">
|
||||
<i data-feather="inbox" class="w-12 h-12 text-gray-400 mx-auto mb-4"></i>
|
||||
<p class="text-gray-500">업무일지 데이터가 없습니다.</p>
|
||||
<i data-feather="inbox" class="w-12 h-12 text-white/60 mx-auto mb-4"></i>
|
||||
<p class="text-white/70">업무일지 데이터가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
<div id="pagination" class="mt-6 flex items-center justify-between">
|
||||
<div id="pagination" class="mt-6 flex items-center justify-between glass-effect rounded-lg p-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-700">페이지당 행 수:</span>
|
||||
<select id="pageSize" class="border border-gray-300 rounded-md px-2 py-1 text-sm">
|
||||
<span class="text-sm text-white/80">페이지당 행 수:</span>
|
||||
<select id="pageSize" class="bg-white/20 border border-white/30 rounded-md px-2 py-1 text-sm text-white">
|
||||
<option value="10">10</option>
|
||||
<option value="25" selected>25</option>
|
||||
<option value="50">50</option>
|
||||
@@ -218,16 +320,16 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button id="prevPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<button id="prevPage" class="px-3 py-1 border border-white/30 rounded-md text-sm text-white hover:bg-white/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
이전
|
||||
</button>
|
||||
<span id="pageInfo" class="text-sm text-gray-700">1 / 1</span>
|
||||
<button id="nextPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span id="pageInfo" class="text-sm text-white/80">1 / 1</span>
|
||||
<button id="nextPage" class="px-3 py-1 border border-white/30 rounded-md text-sm text-white hover:bg-white/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 상세 모달 -->
|
||||
<div id="detailModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
||||
@@ -247,6 +349,123 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 공통 네비게이션 컴포넌트
|
||||
class CommonNavigation {
|
||||
constructor(currentPage = '') {
|
||||
this.currentPage = currentPage;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createNavigation();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
createNavigation() {
|
||||
const nav = document.createElement('nav');
|
||||
nav.className = 'glass-effect border-b border-white/10';
|
||||
nav.style.cssText = `
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
`;
|
||||
nav.innerHTML = this.getNavigationHTML();
|
||||
|
||||
// body의 첫 번째 자식으로 추가
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
}
|
||||
|
||||
getNavigationHTML() {
|
||||
return `
|
||||
<div class="container mx-auto px-4" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 로고/타이틀 -->
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-xl font-bold text-white">GroupWare</h2>
|
||||
</div>
|
||||
|
||||
<!-- 메뉴 -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 버튼 -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 -->
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
|
||||
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
getMobileMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
// 모바일 메뉴 토글
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 내비게이션 초기화
|
||||
function initNavigation(currentPage = '') {
|
||||
// DOM이 로드된 후에 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new CommonNavigation(currentPage);
|
||||
});
|
||||
} else {
|
||||
new CommonNavigation(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 변수
|
||||
let jobData = [];
|
||||
let filteredData = [];
|
||||
@@ -257,6 +476,8 @@
|
||||
|
||||
// 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 네비게이션 초기화
|
||||
initNavigation('jobreport');
|
||||
initializeApp();
|
||||
loadJobData();
|
||||
});
|
||||
@@ -465,7 +686,7 @@
|
||||
|
||||
pageData.forEach((item, index) => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 cursor-pointer';
|
||||
row.className = 'hover:bg-white/10 cursor-pointer transition-colors';
|
||||
row.setAttribute('data-item-id', item.idx || index);
|
||||
row.addEventListener('click', () => toggleRowDetail(item, row));
|
||||
|
||||
@@ -474,9 +695,9 @@
|
||||
`${item.otStart} ~ ${item.otEnd}` : '-';
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
<div class="flex items-center">
|
||||
<i data-feather="chevron-right" class="w-4 h-4 mr-2 text-gray-400 expand-icon"></i>
|
||||
<i data-feather="chevron-right" class="w-4 h-4 mr-2 text-white/60 expand-icon"></i>
|
||||
${formatDate(item.pdate)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -485,35 +706,35 @@
|
||||
${item.status || '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.projectName || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.requestpart || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.package || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.type || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.process || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">
|
||||
<div class="max-w-xs truncate cursor-pointer hover:text-primary hover:underline"
|
||||
<td class="px-6 py-4 text-sm text-white">
|
||||
<div class="max-w-xs truncate cursor-pointer hover:text-white/80 hover:underline"
|
||||
title="${item.description || ''}"
|
||||
onclick="event.stopPropagation(); showDetailModal(${JSON.stringify(item).replace(/"/g, '"')})">
|
||||
${item.description || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${otTime}
|
||||
</td>
|
||||
`;
|
||||
|
||||
@@ -14,23 +14,97 @@
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3B82F6',
|
||||
secondary: '#6B7280',
|
||||
success: '#10B981',
|
||||
danger: '#EF4444',
|
||||
warning: '#F59E0B'
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
success: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
},
|
||||
warning: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f',
|
||||
},
|
||||
danger: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #3B82F6;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@@ -45,41 +119,62 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
/* 스크롤바 스타일링 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: var(--scrollbar-width, 16px);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<body class="gradient-bg min-h-screen">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="text-center mb-8 animate-fade-in">
|
||||
<h1 class="text-4xl font-bold text-white mb-2">근태관리</h1>
|
||||
<p class="text-white/80 text-lg">출퇴근 시간 및 휴가 관리</p>
|
||||
</div>
|
||||
|
||||
<!-- 개발중 경고 메시지 -->
|
||||
<div class="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
||||
<p class="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 검색 및 필터 섹션 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<div class="glass-effect rounded-lg p-6 mb-6 animate-slide-up">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="startDate" class="block text-sm font-medium text-gray-700 mb-2">시작일</label>
|
||||
<input type="date" id="startDate" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<label for="startDate" class="block text-sm font-medium text-white/80 mb-2">시작일</label>
|
||||
<input type="date" id="startDate" class="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label for="endDate" class="block text-sm font-medium text-gray-700 mb-2">종료일</label>
|
||||
<input type="date" id="endDate" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<label for="endDate" class="block text-sm font-medium text-white/80 mb-2">종료일</label>
|
||||
<input type="date" id="endDate" class="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button id="searchBtn" class="w-full bg-primary text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors duration-200 flex items-center justify-center">
|
||||
<button id="searchBtn" class="w-full glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 flex items-center justify-center">
|
||||
<span id="searchBtnText">조회</span>
|
||||
<div id="searchBtnLoading" class="loading ml-2 hidden"></div>
|
||||
</button>
|
||||
@@ -88,66 +183,66 @@
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6 animate-slide-up">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-blue-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="p-3 bg-primary-500/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">휴가사용</p>
|
||||
<p id="totalDays" class="text-2xl font-bold text-gray-900">0</p>
|
||||
<p class="text-sm font-medium text-white/80">휴가사용</p>
|
||||
<p id="totalDays" class="text-2xl font-bold text-white">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-green-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="p-3 bg-success-500/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">대체사용</p>
|
||||
<p id="normalDays" class="text-2xl font-bold text-gray-900">0</p>
|
||||
<p class="text-sm font-medium text-white/80">대체사용</p>
|
||||
<p id="normalDays" class="text-2xl font-bold text-white">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-yellow-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="p-3 bg-warning-500/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">잔량(년차)</p>
|
||||
<p id="lateDays" class="text-2xl font-bold text-gray-900">0</p>
|
||||
<p class="text-sm font-medium text-white/80">잔량(년차)</p>
|
||||
<p id="lateDays" class="text-2xl font-bold text-white">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="glass-effect rounded-lg p-6 card-hover">
|
||||
<div class="flex items-center">
|
||||
<div class="p-2 bg-red-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="p-3 bg-danger-500/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">잔량(대체)</p>
|
||||
<p id="absentDays" class="text-2xl font-bold text-gray-900">0</p>
|
||||
<p class="text-sm font-medium text-white/80">잔량(대체)</p>
|
||||
<p id="absentDays" class="text-2xl font-bold text-white">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 데이터 테이블 -->
|
||||
<div class="bg-white rounded-lg shadow-md">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">근태 상세 내역</h3>
|
||||
<button id="addBtn" class="bg-primary text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors duration-200 flex items-center">
|
||||
<div class="glass-effect rounded-lg animate-slide-up custom-scrollbar">
|
||||
<div class="px-6 py-4 border-b border-white/20 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-white">근태 상세 내역</h3>
|
||||
<button id="addBtn" class="glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
@@ -155,45 +250,45 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50 sticky top-0">
|
||||
<table class="min-w-full divide-y divide-white/20">
|
||||
<thead class="bg-white/10 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">구분</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">시작일</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">종료일</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">사번</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">성명</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">사용(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">사용(H)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">발생(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">발생(H)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">구분</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">시작일</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">종료일</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사번</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">성명</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사용(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사용(H)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">발생(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">발생(H)</th>
|
||||
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">내용</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">내용</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">#</th>
|
||||
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">잔량(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">잔량(H)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">전일(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">전일(H)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">잔량(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">잔량(H)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">전일(일)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">전일(H)</th>
|
||||
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">소스</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록자</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록일</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">소스</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">등록자</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">등록일</th>
|
||||
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dataTableBody" class="bg-white divide-y divide-gray-200">
|
||||
<tbody id="dataTableBody" class="divide-y divide-white/10">
|
||||
<tr id="loadingRow" class="hidden">
|
||||
<td colspan="7" class="px-6 py-4 text-center">
|
||||
<td colspan="18" class="px-6 py-4 text-center">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="loading mr-2"></div>
|
||||
<span class="text-gray-500">데이터를 불러오는 중...</span>
|
||||
<span class="text-white/80">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="noDataRow" class="hidden">
|
||||
<td colspan="7" class="px-6 py-4 text-center text-gray-500">
|
||||
<td colspan="18" class="px-6 py-4 text-center text-white/70">
|
||||
조회된 데이터가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -291,12 +386,131 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 공통 네비게이션 컴포넌트
|
||||
class CommonNavigation {
|
||||
constructor(currentPage = '') {
|
||||
this.currentPage = currentPage;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createNavigation();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
createNavigation() {
|
||||
const nav = document.createElement('nav');
|
||||
nav.className = 'glass-effect border-b border-white/10';
|
||||
nav.style.cssText = `
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
`;
|
||||
nav.innerHTML = this.getNavigationHTML();
|
||||
|
||||
// body의 첫 번째 자식으로 추가
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
}
|
||||
|
||||
getNavigationHTML() {
|
||||
return `
|
||||
<div class="container mx-auto px-4" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 로고/타이틀 -->
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-xl font-bold text-white">GroupWare</h2>
|
||||
</div>
|
||||
|
||||
<!-- 메뉴 -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 버튼 -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 -->
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
|
||||
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
getMobileMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
// 모바일 메뉴 토글
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 내비게이션 초기화
|
||||
function initNavigation(currentPage = '') {
|
||||
// DOM이 로드된 후에 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new CommonNavigation(currentPage);
|
||||
});
|
||||
} else {
|
||||
new CommonNavigation(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 변수
|
||||
let currentData = [];
|
||||
let currentEditId = null;
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 네비게이션 초기화
|
||||
initNavigation('kuntae');
|
||||
initializeDates();
|
||||
loadData();
|
||||
setupEventListeners();
|
||||
@@ -423,41 +637,41 @@
|
||||
|
||||
currentData.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 cursor-pointer';
|
||||
row.className = 'hover:bg-white/10 cursor-pointer transition-colors';
|
||||
row.setAttribute('data-id', item.idx);
|
||||
|
||||
const startDate = item.sdate ? new Date(item.sdate) : null;
|
||||
const endDate = item.edate ? new Date(item.edate) : null;
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.cate || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${startDate ? formatDate(startDate) : '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${endDate ? formatDate(endDate) : '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.uid || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.uname || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.term || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.termdr || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.drtime || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.crtime || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 max-w-xs truncate" title="${item.contents || ''}">${item.contents || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.cate || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${startDate ? formatDate(startDate) : '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${endDate ? formatDate(endDate) : '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.uid || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.uname || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.term || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.termdr || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.drtime || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.crtime || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white/80 max-w-xs truncate" title="${item.contents || ''}">${item.contents || '-'}</td>
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.tag || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> </td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> </td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> </td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> </td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.tag || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white"> </td>
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.extcate || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.wuid || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${item.wdate || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.extcate || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.wuid || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.wdate || '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white/80">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-blue-600 hover:text-blue-800 edit-btn" data-id="${item.idx}">
|
||||
<button class="text-blue-400 hover:text-blue-300 edit-btn transition-colors" data-id="${item.idx}">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-red-600 hover:text-red-800 delete-btn" data-id="${item.idx}">
|
||||
<button class="text-red-400 hover:text-red-300 delete-btn transition-colors" data-id="${item.idx}">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
|
||||
223
Project/Web/wwwroot/css/common.css
Normal file
223
Project/Web/wwwroot/css/common.css
Normal file
@@ -0,0 +1,223 @@
|
||||
/* GroupWare 공통 스타일 */
|
||||
|
||||
/* Glass Effect */
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
/* Gradient Background */
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* Card Hover Effect */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: var(--scrollbar-width, 16px);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Common Animations */
|
||||
@keyframes fadeIn {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% { transform: translateY(10px); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Select Box Styling */
|
||||
select option {
|
||||
background-color: #374151 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
select option:hover {
|
||||
background-color: #4B5563 !important;
|
||||
}
|
||||
|
||||
select option:checked {
|
||||
background-color: #6366F1 !important;
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Development Warning Message */
|
||||
.dev-warning {
|
||||
background: #f97316;
|
||||
border-left: 4px solid #ea580c;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dev-warning .icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #9a3412;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.dev-warning .title {
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dev-warning .description {
|
||||
color: #fed7aa;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Common Button Styles */
|
||||
.btn-glass {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-glass:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Form Input Styles */
|
||||
.input-glass {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-glass::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.input-glass:focus {
|
||||
outline: none;
|
||||
ring: 2px;
|
||||
ring-color: rgba(255, 255, 255, 0.5);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table-glass {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-glass thead {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.table-glass th {
|
||||
padding: 0.75rem 1.5rem;
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.table-glass tbody {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.table-glass tr {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.table-glass tr:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.table-glass td {
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-white-80 {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.text-white-60 {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.bg-white-10 {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.bg-white-20 {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.border-white-30 {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
111
Project/Web/wwwroot/js/common-nav.js
Normal file
111
Project/Web/wwwroot/js/common-nav.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// 공통 네비게이션 컴포넌트
|
||||
class CommonNavigation {
|
||||
constructor(currentPage = '') {
|
||||
this.currentPage = currentPage;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createNavigation();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
createNavigation() {
|
||||
const nav = document.createElement('nav');
|
||||
nav.className = 'glass-effect border-b border-white/10';
|
||||
nav.innerHTML = this.getNavigationHTML();
|
||||
|
||||
// body의 첫 번째 자식으로 추가
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
}
|
||||
|
||||
getNavigationHTML() {
|
||||
return `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 로고/타이틀 -->
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-xl font-bold text-white">GroupWare</h2>
|
||||
</div>
|
||||
|
||||
<!-- 메뉴 -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 버튼 -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 -->
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
|
||||
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
getMobileMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
// 모바일 메뉴 토글
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 내비게이션 초기화
|
||||
function initNavigation(currentPage = '') {
|
||||
// DOM이 로드된 후에 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new CommonNavigation(currentPage);
|
||||
});
|
||||
} else {
|
||||
new CommonNavigation(currentPage);
|
||||
}
|
||||
}
|
||||
133
Project/Web/wwwroot/js/common-navigation.js
Normal file
133
Project/Web/wwwroot/js/common-navigation.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// 공통 네비게이션 컴포넌트
|
||||
class CommonNavigation {
|
||||
constructor(currentPage = '') {
|
||||
this.currentPage = currentPage;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createNavigation();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
createNavigation() {
|
||||
const nav = document.createElement('nav');
|
||||
nav.className = 'glass-effect border-b border-white/10';
|
||||
nav.style.cssText = `
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
`;
|
||||
nav.innerHTML = this.getNavigationHTML();
|
||||
|
||||
// body의 첫 번째 자식으로 추가
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
}
|
||||
|
||||
getNavigationHTML() {
|
||||
return `
|
||||
<div class="container mx-auto px-4" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 로고/타이틀 -->
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-xl font-bold text-white">GroupWare</h2>
|
||||
</div>
|
||||
|
||||
<!-- 메뉴 -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 버튼 -->
|
||||
<div class="md:hidden">
|
||||
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 메뉴 -->
|
||||
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
|
||||
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
|
||||
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
|
||||
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
|
||||
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
getMobileMenuItemHTML(pageKey, href, text, svgPath) {
|
||||
const isActive = this.currentPage === pageKey;
|
||||
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
||||
|
||||
return `
|
||||
<a href="${href}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${svgPath}"></path>
|
||||
</svg>
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
// 모바일 메뉴 토글
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 내비게이션 초기화
|
||||
function initNavigation(currentPage = '') {
|
||||
// DOM이 로드된 후에 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new CommonNavigation(currentPage);
|
||||
});
|
||||
} else {
|
||||
new CommonNavigation(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
// 개발중 경고 메시지 생성 함수
|
||||
function createDevWarning() {
|
||||
return `
|
||||
<div class="dev-warning animate-slide-up">
|
||||
<div class="flex items-center">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="title">🚧 개발중인 기능입니다</p>
|
||||
<p class="description">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
77
Project/Web/wwwroot/js/tailwind-config.js
Normal file
77
Project/Web/wwwroot/js/tailwind-config.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// GroupWare 공통 Tailwind 설정
|
||||
window.tailwindConfig = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
success: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
},
|
||||
warning: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f',
|
||||
},
|
||||
danger: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tailwind 설정 적용
|
||||
if (typeof tailwind !== 'undefined') {
|
||||
tailwind.config = window.tailwindConfig;
|
||||
}
|
||||
@@ -473,8 +473,17 @@ namespace Project
|
||||
|
||||
private void codesToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var f = new FCM0000.fCode();
|
||||
f.ShowDialog();
|
||||
if (Pub.InitWebView > 0 && System.Diagnostics.Debugger.IsAttached)
|
||||
{
|
||||
var f = new Dialog.fCommon();
|
||||
f.ShowDialog();
|
||||
}
|
||||
else
|
||||
{
|
||||
var f = new FCM0000.fCode();
|
||||
f.ShowDialog();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -105,7 +105,9 @@
|
||||
<Reference Include="RichTextBoxEx, Version=1.0.6541.36291, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\RichTextBoxEx.1.0.0\lib\RichTextBoxEx.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System">
|
||||
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
|
||||
Reference in New Issue
Block a user