Compare commits

...

No commits in common. "main" and "dev" have entirely different histories.
main ... dev

25 changed files with 1662 additions and 57 deletions

2
.gitignore vendored Executable file → Normal file
View File

@ -1,5 +1,5 @@
# 특정 환경에 따라 추가
./private/
/private/
./privacy/
./publish/
publish/

View File

@ -8,8 +8,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Program\Common\Interfaces\" />
</ItemGroup>

View File

@ -1,73 +1,153 @@
//var builder = WebApplication.CreateBuilder(args);
/*
var options = new WebApplicationOptions
using Pomelo.EntityFrameworkCore;
using System.Text;
using AcaMate.Common.Chat;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.SignalR;
using AcaMate.Common.Models;
using AcaMate.V1.Services;
using AcaMate.Common.Data;
using AcaMate.V1.Controllers;
var builder = WebApplication.CreateBuilder(args);
// DB 설정부 시작
builder.Configuration.AddJsonFile("private/dbSetting.json", optional: true, reloadOnChange: true);
// var connectionString = builder.Configuration.GetConnectionString("MariaDbConnection");
// builder.Services.AddDbContext<AppDbContext>(options => options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
builder.Services.AddDbContext<AppDbContext>(optionsAction: (serviceProvider, options) =>
{
WebRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development"
? "/src/publish/Debug/wwwroot"
: "/src/publish/Release/wwwroot"
};
*/
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var dbName = httpContextAccessor.HttpContext?.Request.Query["aca_code"].ToString();
var baseConnectionString = builder.Configuration.GetConnectionString("MariaDbConnection");
if (!string.IsNullOrEmpty(dbName))
{
baseConnectionString = baseConnectionString.Replace("database=AcaMate", $"database={dbName}");
}
// var currentDirectory = Directory.GetCurrentDirectory();
// var webRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development"
// ? Path.Combine(currentDirectory, "publish/Debug/wwwroot")
// : Path.Combine(currentDirectory, "publish/Release/wwwroot");
options.UseMySql(baseConnectionString, ServerVersion.AutoDetect(baseConnectionString));
});
builder.Services.AddHttpContextAccessor();
// DB 설정부 끝
var webRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_WEBROOT");
// builder.WebHost.UseWebRoot(webRootPath);
var options = new WebApplicationOptions { WebRootPath = webRootPath };
var dbString = builder.Configuration.GetConnectionString("MariaDbConnection");
var userString = builder.Configuration.GetConnectionString("DBAccount");
var builder = WebApplication.CreateBuilder(options);
// JWT 설정부 시작
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddJsonFile("private/jwtSetting.Development.json", optional: true, reloadOnChange: true);
}
else
{
builder.Configuration.AddJsonFile("private/jwtSetting.json", optional: true, reloadOnChange: true);
}
// var env = builder.Environment.EnvironmentName;
// string wwwrootPath = env == "Development" ? "/src/publish/Debug/wwwroot" : "/src/publish/Release/wwwroot";
// builder.WebHost.UseWebRoot(wwwrootPath);
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings"));
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var jwtSettings = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
ClockSkew = TimeSpan.FromMinutes(jwtSettings.ClockSkewMinutes)
};
});
// JWT 설정부 끝
builder.Services.AddControllers();
// 여기다가 API 있는 컨트롤러들 AddScoped 하면 되는건가?
builder.Services.AddScoped<AcaMate.Common.Token.JwtTokenService>();
builder.Services.AddScoped<IRepositoryService, AcaMate.V1.Services.RepositoryService>();
// builder.Services.AddScoped<UserService>(); //
// builder.Services.AddScoped<UserController>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 스웨거 설정 추가 부분
// builder.Services.AddSwaggerGen();
builder.Services.AddCustomSwagger();
// SignalR 설정 추가 부분
builder.Services.AddSignalR();
builder.Services.AddCors(option =>
{
option.AddPolicy("CorsPolicy", builder =>
{
builder
.WithOrigins("https://devacamate.ipstein.myds.me", "https://acamate.ipstein.myds.me") // 특정 도메인만 허용
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
// 로그 설정 부분
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
if (builder.Environment.IsDevelopment()) {
builder.Logging.SetMinimumLevel(LogLevel.Trace);
}
else
{
builder.Logging.SetMinimumLevel(LogLevel.Warning);
}
// 로컬 테스트 위한 부분 (올릴때는 꺼두기)
// builder.WebHost.UseUrls("http://0.0.0.0:5144");
///// ===== builder 설정 부 ===== /////
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
// app.UseSwagger();
// app.UseSwaggerUI();
app.UseCustomSwaggerUI();
app.UseDeveloperExceptionPage(); // 좀더 자세한 예외 정보 제공
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
app.UseExceptionHandler("/Error");
// .UseStaticFiles()
app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true
});
// 로컬 테스트 위한 부분 (올릴떄는 켜두기)
app.UseHttpsRedirection();
app.UseRouting();
app.MapFallbackToFile("index.html");
// app.MapControllers();
var summaries = new[]
app.UseCors("CorsPolicy");
app.UseAuthorization();
app.UseWebSockets();
app.UseEndpoints(end =>
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
end.MapControllers();
end.MapHub<ChatHub>("/chatHub");
});
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
app.Run();

View File

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace AcaMate.Common.Chat;
public class ChatHub : Hub
{
// 클라이언트에서 메시지를 보내면 모든 사용자에게 전송
public async Task SendMessage(string user, string message)
{
Console.WriteLine($"Message received: {user}: {message}");
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
// 특정 사용자에게 메시지를 보냄
public async Task SendMessageToUser(string connectionId, string message)
{
await Clients.Client(connectionId).SendAsync("ReceiveMessage", message);
}
// 클라이언트가 연결될 때 호출
public override async Task OnConnectedAsync()
{
await Clients.Caller.SendAsync("ReceiveMessage", "System", $"Welcome! Your ID: {Context.ConnectionId}");
Console.WriteLine("OnConnectedAsync");
await base.OnConnectedAsync();
}
// 클라이언트가 연결 해제될 때 호출
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Clients.All.SendAsync("ReceiveMessage", "System", $"{Context.ConnectionId} disconnected");
Console.WriteLine("OnDisconnectedAsync");
await base.OnDisconnectedAsync(exception);
}
}

View File

@ -0,0 +1,35 @@
using AcaMate.Common.Models;
using Microsoft.EntityFrameworkCore;
using AcaMate.V1.Models;
using Version = AcaMate.V1.Models.Version;
namespace AcaMate.Common.Data;
//database=AcaMate;
public class AppDbContext: DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
//MARK: Program
public DbSet<Version> Version { get; set; }
public DbSet<Academy> Academy { get; set; }
public DbSet<RefreshToken> RefreshTokens { get; set; }
//MARK: USER
public DbSet<Login> Login { get; set; }
public DbSet<User_Academy> UserAcademy { get; set; }
public DbSet<User> User { get; set; }
public DbSet<Permission> Permission { get; set; }
// public DbSet<Token> Token { get; set; }
public DbSet<Location> Location { get; set; }
public DbSet<Contact> Contact { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User_Academy>()
.HasKey(ua => new { ua.uid, ua.bid });
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Collections.Generic;
using AcaMate.Common.Models;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
namespace AcaMate.Common.Token;
public class JwtTokenService
{
private readonly JwtSettings _jwtSettings;
private readonly ILogger<JwtTokenService> _logger;
public JwtTokenService(IOptions<JwtSettings> jwtSettings, ILogger<JwtTokenService> logger)
{
_jwtSettings = jwtSettings.Value;
_logger = logger;
}
public string GenerateJwtToken(string uid)//, string role)
{
// 1. 클레임(Claim) 설정 - 필요에 따라 추가 정보도 포함
var claims = new List<Claim>
{
// 토큰 주체(sub) 생성을 위해 값으로 uid를 사용함 : 토큰이 대표하는 고유 식별자
new Claim(JwtRegisteredClaimNames.Sub, uid),
// Jti 는 토큰 식별자로 토큰의 고유 ID 이다.
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
// jwt 토큰이 가지는 권한
// new Claim(ClaimTypes.Role, role),
// 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin")
};
// 2. 비밀 키와 SigningCredentials 생성
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// 3. 토큰 생성 (Issuer, Audience, 만료 시간, 클레임, 서명 정보 포함)
var token = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(_jwtSettings.ExpiryMinutes),
signingCredentials: credentials
);
// 4. 토큰 객체를 문자열로 변환하여 반환
return new JwtSecurityTokenHandler().WriteToken(token);
}
public RefreshToken GenerateRefreshToken(string uid)
{
var randomNumber = new byte[32]; // 256비트
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
}
// return Convert.ToBase64String(randomNumber);
return new RefreshToken()
{
uid = uid,
refresh_token = Convert.ToBase64String(randomNumber),
create_Date = DateTime.UtcNow,
expire_date = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays)
};
}
public ClaimsPrincipal ValidateToken(string token)
{
if (string.IsNullOrWhiteSpace(token)) return null;
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var key = Encoding.UTF8.GetBytes(_jwtSettings.SecretKey);
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = _jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = _jwtSettings.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(_jwtSettings.ClockSkewMinutes)
};
var principal = tokenHandler.ValidateToken(token, validationParameters, out var securityToken);
return principal;
}
catch (Exception e)
{
Console.WriteLine($"검증 실패 {e}");
return null;
}
}
}

View File

@ -0,0 +1,52 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace AcaMate.Common.Models;
public class JwtSettings
{
public string SecretKey { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public int ExpiryMinutes { get; set; }
public int ClockSkewMinutes { get; set; }
public int RefreshTokenExpiryDays { get; set; }
}
[Table("refresh_token")]
public class RefreshToken
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
public string refresh_token { get; set; }
public DateTime create_Date { get; set; }
public DateTime expire_date { get; set; }
// 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다.
public DateTime? revoke_Date { get; set; }
}
public class ValidateToken
{
public string token { get; set; }
public string refresh { get; set; }
public string uid { get; set; }
}
/*
"""
1. .
2. DB에 .
3. .
4. .
5. (1) .
6. (2) .
7. .
8. .
9. .
10. 2 .
"""
*/

View File

@ -0,0 +1,78 @@
using System.Text.Json;
namespace AcaMate.Common.Models;
public class APIResponseStatus<T>
{
public Status status { get; set; }
public T? data { get; set; }
public string JsonToString()
{
return JsonSerializer.Serialize(this);
}
}
public class Status
{
public string code { get; set; }
public string message { get; set; }
}
public static class DefaultResponse
{
// private static readonly Lazy<ErrorResponse> _instance = new Lazy<ErrorResponse>();
// public static ErrorResponse Instace => _instance.Value;
// private ErrorResponse()
// {
// // 외부 초기화 방지
// }
public static APIResponseStatus<string> Success = new APIResponseStatus<string>
{
status = new Status()
{
code = "000",
message = "정상"
}
};
public static APIResponseStatus<string> InvalidInputError = new APIResponseStatus<string>
{
status = new Status()
{
code = "001",
message = "입력 값이 유효하지 않습니다."
}
};
public static APIResponseStatus<string> NotFoundError = new APIResponseStatus<string>
{
status = new Status()
{
code = "002",
message = "알맞은 값을 찾을 수 없습니다."
}
};
public static APIResponseStatus<string> InternalSeverError = new APIResponseStatus<string>
{
status = new Status
{
code = "003",
message = "통신에 오류가 발생하였습니다."
}
};
public static APIResponseStatus<string> UnknownError = new APIResponseStatus<string>
{
status = new Status()
{
code = "999",
message = "알 수 없는 오류가 발생하였습니다.."
}
};
}

View File

@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using AcaMate.Common.Data;
using AcaMate.Common.Models;
using Microsoft.IdentityModel.Tokens;
using Version = AcaMate.V1.Models.Version;
namespace AcaMate.V1.Controllers;
[ApiController]
[Route("/api/v1/in/app")]
[ApiExplorerSettings(GroupName = "공통")]
public class AppController : ControllerBase
{
private readonly AppDbContext _dbContext;
public AppController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet("version")]
[CustomOperation("앱 버전 확인","앱 버전을 확인해서 업데이트 여부 판단", "시스템")]
public IActionResult GetVersionData(string type)
{
if (string.IsNullOrEmpty(type))
{
return BadRequest(DefaultResponse.InvalidInputError);
}
try
{
var version = _dbContext.Version.FirstOrDefault(v => v.os_type == (type == "I" ? "VO01" : "VO02"));
if (version == null)
{
return NotFound(DefaultResponse.NotFoundError);
}
var response = new APIResponseStatus<Version>
{
status = new Status()
{
code = "000",
message = "정상"
},
data = new Version()
{
os_type = (version.os_type == "VO01" ? "I" : (version.os_type == "VO02" ? "A" : "W")),
final_ver = version.final_ver,
force_ver = version.force_ver,
dev_ver = version.dev_ver,
choice_update_yn = version.choice_update_yn
}
};
string jsonString = JsonSerializer.Serialize(response);
// return Ok(jsonString);
return Ok(response.JsonToString());
}
catch (Exception ex)
{
Console.WriteLine($"{ex.Message}\n{ex.StackTrace}");
return StatusCode(500, DefaultResponse.UnknownError);
}
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace AcaMate.V1.Controllers;
[ApiController]
[Route("/api/error")]
public class ErrorController: ControllerBase
{
[HttpGet]
public IActionResult HandleError()
{
return Problem("오류가 발생하였습니다. 잠시후 다시 시도해주세요.");
}
}

View File

@ -0,0 +1,43 @@
using System.Text.Json;
using AcaMate.Common.Data;
using AcaMate.Common.Models;
using AcaMate.V1.Models;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
namespace AcaMate.V1.Controllers;
[ApiController]
[Route("/api/v1/in/member")]
[ApiExplorerSettings(GroupName = "사업자 정보")]
public class MemberController: ControllerBase
{
private readonly AppDbContext _dbContext;
public MemberController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet("business")]
public IActionResult GetBusinessData()
{
// return Ok("GOOD");
return Ok("DB 참조");
}
// -- -- -- -- -- -- -- -- -- -- -- -- //
[HttpGet("/api/v1/out/member/business")]
public IActionResult SearchBusinessNo()
{
return Ok("외부 참조");
}
}

View File

@ -0,0 +1,105 @@
using AcaMate.Common.Models;
using AcaMate.V1.Models;
using Microsoft.AspNetCore.Mvc;
using AcaMate.V1.Services;
namespace AcaMate.V1.Controllers;
[ApiController]
[Route("/api/v1/in/push")]
[ApiExplorerSettings(GroupName = "공통")]
public class PushController : ControllerBase
{
private readonly IWebHostEnvironment _env;
public PushController(IWebHostEnvironment env)
{
_env = env;
}
[HttpGet()]
[CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다..", "푸시")]
public IActionResult GetPushData()
{
return Ok("SEND");
}
/// <summary>
/// Sends a push notification to the specified device token with the provided payload.
/// </summary>
/// <param name="deviceToken">The device token to send the notification to.</param>
/// <param name="payload">The payload of the push notification.</param>
/// <returns>An IActionResult indicating the result of the operation.</returns>
/// <response code="200">Push notification sent successfully.</response>
/// <response code="400">Invalid input parameters.</response>
/// <response code="500">Internal server error occurred.</response>
/// <response code="999">Service unavailable.</response>
[HttpPost("send")]
[CustomOperation("푸시전송", "저장된 양식으로, 사용자에게 푸시를 전송한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> SendPush(string deviceToken, [FromBody] Payload? payload)
{
if (string.IsNullOrWhiteSpace(deviceToken))
{
var inputError = DefaultResponse.InvalidInputError;
inputError.status.message = "Deviece Toekn 오류";
return StatusCode(500,inputError);
}
if (payload == null)
{
var inputError = DefaultResponse.InvalidInputError;
inputError.status.message = "payload 입력 오류";
return StatusCode(500,inputError);
}
string uri = "";
string p12Path = "";
string p12PWPath = "/src/private/appleKeys.json";
// string p12PWPath = "private/appleKeys.json";
string apnsTopic = "me.myds.ipstein.acamate.AcaMate";
if (_env.IsDevelopment())
{
uri = "https://api.sandbox.push.apple.com/";
p12Path = "/src/private/AM_Push_Sandbox.p12";
// p12Path = "private/AM_Push_Sandbox.p12";
}
else
{
uri = "https://api.push.apple.com/";
p12Path = "/src/private/AM_Push.p12";
// p12Path = "private/AM_Push.p12";
}
// ApnsPushService 인스턴스 생성
var pushService = new ApnsPushService(uri, p12Path, p12PWPath, apnsTopic);
// 푸시 알림 전송
var result = await pushService.SendPushNotificationAsync(deviceToken, payload);
if (result.Success)
{
return Ok(DefaultResponse.Success.JsonToString());
}
else
{
var apnsError = DefaultResponse.InternalSeverError;
apnsError.status.message = $"{result.Message}";
return result.Code switch
{
"002" => StatusCode(002, apnsError),
"003" => StatusCode(003, apnsError),
"999" => StatusCode(999, apnsError)
};
}
}
}

View File

@ -0,0 +1,311 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore.Query;
using MySqlConnector;
using System.Linq;
using AcaMate.Common.Data;
using AcaMate.Common.Token;
using AcaMate.Common.Models;
using AcaMate.V1.Models;
using AcaMate.V1.Services;
namespace AcaMate.V1.Controllers;
[ApiController]
[Route("/api/v1/in/user")]
[ApiExplorerSettings(GroupName = "사용자")]
public class UserController : ControllerBase
{
private readonly AppDbContext _dbContext;
private readonly ILogger<UserController> _logger;
private readonly JwtTokenService _jwtTokenService;
private readonly IRepositoryService _repositoryService;
public UserController(AppDbContext dbContext, ILogger<UserController> logger, JwtTokenService jwtTokenService, IRepositoryService repositoryService)
{
_dbContext = dbContext;
_logger = logger;
_jwtTokenService = jwtTokenService;
_repositoryService = repositoryService;
}
[HttpGet]
[CustomOperation("회원 정보 조회", "회원 정보 조회", "사용자")]
public IActionResult GetUserData(string uid)
{
if (string.IsNullOrEmpty(uid)) return BadRequest(DefaultResponse.InvalidInputError);
try
{
var user = _dbContext.User
.Where(u => u.uid == uid)
.Select(u => new User
{
uid = u.uid,
name = u.name,
auto_login_yn = u.auto_login_yn,
birth = u.birth,
device_id = u.device_id,
login_date = u.login_date,
type = u.type
})
.FirstOrDefault();
var response = new APIResponseStatus<User>
{
status = new Status
{
code = "000",
message = "정상"
},
data = user
};
return Ok(response.JsonToString());
}
catch (Exception ex)
{
return StatusCode(500, DefaultResponse.UnknownError);
}
}
[HttpGet("login")]
[CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")]
public async Task<IActionResult> Login(string acctype, string sns_id)
{
// API 동작 파라미터 입력 값 확인
if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id))
return BadRequest(DefaultResponse.InvalidInputError);
try
{
var login = await _dbContext.Login
.FirstOrDefaultAsync(l => l.sns_type == acctype && l.sns_id == sns_id);
if (login != null)
{
// 로그인 정보가 존재 하는 상황
var uid = login.uid;
var user = await _dbContext.User
.FirstOrDefaultAsync(u => u.uid == uid);
if (user != null)
{
// 정상적으로 User 테이블에도 있는것이 확인 됨
user.login_date = DateTime.Now;
await _dbContext.SaveChangesAsync();
// 토큰 생성은 로그인이 이제 되고 나서 한다.
var accessToken = _jwtTokenService.GenerateJwtToken(uid);//, "Normal");
var refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
_logger.LogInformation($"{uid}: {accessToken}, {refreshToken}");
await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
var response = new APIResponseStatus<dynamic>
{
status = new Status
{
code = "000",
message = "정상"
},
data = new
{
token = accessToken,
refresh = refreshToken.refresh_token
}
};
return Ok(response.JsonToString());
}
}
// case 1: Login 테이블에 값이 없다 == 로그인이 처음
// case 2: User 테이블에 값이 없다 == 이건 문제가 있는 상황 -> 해결은 회원가입 재 진행 시도
// Login에는 있는데 User 테이블에 없다? 말이 안되긴 하는데...
return Ok(new APIResponseStatus<dynamic>
{
status = new Status
{
code = "010",
message = "로그인 정보 없음 > 회원 가입 진행"
},
data = new
{
token = "",
refresh = ""
// bidList = new string[] { }
}
}.JsonToString());
}
catch (Exception ex)
{
_logger.LogInformation($"[로그인] 에러 발생 : {ex}");
return StatusCode(500, DefaultResponse.UnknownError);
}
}
[HttpGet("academy")]
[CustomOperation("학원 리스트 확인", "사용자가 등록된 학원 리스트 확인", "사용자")]
public async Task<IActionResult> ReadAcademyInfo(string token, string refresh)
{
_logger.LogInformation($"토큰 : {token}, {refresh}");
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refresh))
{
var error = DefaultResponse.InvalidInputError;
return Ok(error);
}
try
{
var validateToken = await _repositoryService.ValidateToken(token, refresh);
var uid = validateToken.uid;
var userAcademy = await _dbContext.UserAcademy
.Where(ua => ua.uid == uid)
.Select(ua => ua.bid)
.ToListAsync();
var academies = await _dbContext.Academy
.Where(a => userAcademy.Contains(a.bid))
.Select(a => new AcademyName
{
bid = a.bid,
name = a.business_name
})
.ToListAsync();
var response = new APIResponseStatus<List<AcademyName>>
{
status = new Status
{
code = "000",
message = "정상"
},
data = academies
};
return Ok(response);
}
catch (SecurityTokenException tokenEx)
{
_logger.LogInformation($"[로그인][오류] 토큰 검증 : {tokenEx}");
return StatusCode(500, DefaultResponse.InvalidInputError);
}
catch (Exception ex)
{
_logger.LogInformation($"[로그인][오류] 에러 발생 : {ex}");
return StatusCode(500, DefaultResponse.UnknownError);
}
}
[HttpPost("register")]
[CustomOperation("회원 가입", "사용자 회원 가입", "사용자")]
public async Task<IActionResult> UserRegister([FromBody] UserAll request)
{
if (!ModelState.IsValid) return BadRequest(DefaultResponse.InvalidInputError);
var atIndext = request.email.IndexOf('@');
var localPart_email = request.email.Substring(0, atIndext);
var uid = $"AM{localPart_email}{DateTime.Now:yyyyMMdd}";
var user = new User
{
uid = uid,
name = request.name,
birth = request.birth,
type = request.type,
device_id = request.device_id,
auto_login_yn = request.auto_login_yn,
login_date = request.login_date
};
var login = new Login
{
uid = uid,
sns_id = request.sns_id,
sns_type = request.sns_type
};
var permission = new Permission
{
uid = uid,
location_yn = request.location_yn,
camera_yn = request.camera_yn,
photo_yn = request.photo_yn,
push_yn = request.push_yn,
market_app_yn = request.market_app_yn,
market_sms_yn = request.market_sms_yn,
market_email_yn = request.market_email_yn
};
var contact = new Contact
{
uid = uid,
email = request.email,
phone = request.phone,
address = request.address
};
if (await _repositoryService.SaveData<User, string>(user, u => u.uid))
{
await _repositoryService.SaveData<Login, string>(login, l => l.sns_id);
await _repositoryService.SaveData<Permission, string>(permission, p => p.uid);
await _repositoryService.SaveData<Contact, string>(contact, c => c.uid);
}
// TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함
var token = _jwtTokenService.GenerateJwtToken(uid);//, "admin");
var refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
var result = new APIResponseStatus<dynamic>()
{
status = new Status()
{
code = "000",
message = "정상"
},
data = new
{
accessToken = token,
refreshToken = refreshToken.refresh_token
}
};
return Ok(result.JsonToString());
}
[HttpGet("logout")]
[CustomOperation("로그아웃", "사용자 로그아웃", "사용자")]
public async Task<IActionResult> LogOut(string token, string refresh) //([FromBody] UserAll request)
{
/* */
// var value = await ValidateToken(token, refresh);
// _logger.LogInformation(value.uid);
// _logger.LogInformation(value.refresh);
// _logger.LogInformation(value.token);
/* */
return Ok("로그아웃");
}
}

View File

@ -0,0 +1,15 @@
using System.Text.Json;
namespace AcaMate.V1.Models;
public class APIResult
{
public bool Success { get; set; }
public string Code { get; set; }
public string Message { get; set; }
public string JsonToString()
{
return JsonSerializer.Serialize(this);
}
}

View File

@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AcaMate.V1.Models;
[Table("academy")]
public class Academy
{
[Key]
public string bid { get; set; }
public string business_name { get; set; }
public string business_owner { get; set; }
public string business_number { get; set; }
public DateTime business_date { get; set; }
public string business_address { get; set; }
public string business_contact { get; set; }
public string uid { get; set; }
}
// -- -- -- -- -- DB 테이블 -- -- -- -- -- //
public class AcademyName
{
public string bid { get; set; }
public string name { get; set; }
}
public class RequestAcademy
{
public List<string> bids { get; set; }
}

View File

@ -0,0 +1,41 @@
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography.X509Certificates;
namespace AcaMate.V1.Models;
public class Payload
{
public Aps aps { get; set; }
// public string customKey { get; set; } 이런식으로 추가도 가능
public string ToJson()
{
return System.Text.Json.JsonSerializer.Serialize(this);
}
}
public class Aps
{
public Aps()
{
sound = "default";
content_available = 1;
}
[Required(ErrorMessage = "필수 입력 누락 (alert)")]
public Alert alert { get; set; }
[Required(ErrorMessage = "필수 입력 누락 (badge")]
public int badge { get; set; } // 앱 아이콘 표시 배지 숫자 설정
public string sound { get; set; } // 사운드 파일 이름 default = "default"
public int content_available { get; set; } // 백그라운드 알림 활성화: 필수 (1)
public string? category { get; set; } // 알림에 대한 특정 액션을 정의
}
public class Alert
{
[Required(ErrorMessage = "필수 입력 누락 (title")]
public string title { get; set; } // 제목
[Required(ErrorMessage = "필수 입력 누락 (body)")]
public string body { get; set; } // 내용
public string? subtitle { get; set; } // 부제목 (선택)
}

150
Program/V1/Models/User.cs Normal file
View File

@ -0,0 +1,150 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.InteropServices.JavaScript;
namespace AcaMate.V1.Models;
[Table("login")]
public class Login
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(100)]
public string sns_id {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public string uid {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(4)]
public string sns_type {get; set;}
}
[Table("user_academy")]
public class User_Academy
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string bid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public DateTime register_date { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public bool status { get; set; }
}
[Table("user")]
public class User
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string name { get; set; }
public DateTime? birth { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string type { get; set; }
public string? device_id { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public bool auto_login_yn { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public DateTime login_date { get; set; }
}
[Table("permission")]
public class Permission
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
public bool location_yn {get; set;}
public bool camera_yn {get; set;}
public bool photo_yn {get; set;}
public bool push_yn {get; set;}
public bool market_app_yn {get; set;}
public bool market_sms_yn {get; set;}
public bool market_email_yn {get; set;}
}
[Table("location")]
public class Location
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string lat { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string lng { get; set; }
}
[Table("contact")]
public class Contact
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string email { get; set; }
public string? phone { get; set; }
public string? address { get; set; }
}
// -- -- -- -- -- DB 테이블 -- -- -- -- -- //
public class UserAll
{
// [Required(ErrorMessage = "필수 항목 누락")]
// public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string name { get; set; }
public DateTime? birth { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string type { get; set; }
public string? device_id { get; set; }
public bool auto_login_yn { get; set; }
public DateTime login_date { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string email { get; set; }
public string? phone { get; set; }
public string? address { get; set; }
public bool location_yn {get; set;}
public bool camera_yn {get; set;}
public bool photo_yn {get; set;}
public bool push_yn {get; set;}
public bool market_app_yn {get; set;}
public bool market_sms_yn {get; set;}
public bool market_email_yn {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
public string sns_id {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
public string sns_type {get; set;}
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AcaMate.V1.Models;
[Table("version")]
public class Version
{
[Key]
[MaxLength(4)]
public string os_type { get; set; }
[MaxLength(8)]
public string final_ver { get; set; }
[MaxLength(8)]
public string dev_ver { get; set; }
[MaxLength(8)]
public string force_ver { get; set; }
public bool choice_update_yn { get; set; }
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using AcaMate.Common.Data;
using AcaMate.Common.Models;
using AcaMate.V1.Models;
namespace AcaMate.V1.Repositories;
public class UserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context) {
_context = context;
}
/*
public async Task<IEnumerable<UserAcademyResult>> GetUserAcademyInfoBySnsIdAsync(string snsId)
{
}*/
}

View File

@ -0,0 +1,126 @@
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using AcaMate.V1.Models;
using Version = System.Version;
public class ApnsPushService
{
private readonly string _uri;
private readonly string _p12Path;
private readonly string _p12PWPath;
private readonly string _apnsTopic;
public ApnsPushService(string uri,string p12Path, string p12PWPath, string apnsTopic)
{
_uri = uri ?? throw new ArgumentNullException(nameof(uri));
_p12Path = p12Path ?? throw new ArgumentNullException(nameof(p12Path));
_p12PWPath = p12PWPath ?? throw new ArgumentNullException(nameof(p12PWPath));
_apnsTopic = apnsTopic ?? throw new ArgumentNullException(nameof(apnsTopic));
}
public async Task<APIResult> SendPushNotificationAsync(string deviceToken, Payload payload)
{
// 존재 안하면 예외 던져 버리는거
if (!File.Exists(_p12PWPath))
{
Console.WriteLine($"File not found: {_p12PWPath}");
return new APIResult
{
Success = false ,
Code = "003",
Message = "서버 오류 : p12 PW 파일 확인 필요"
};
}
var jsonPayload = JsonSerializer.Serialize(payload);
var keys = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(_p12PWPath));
var p12Password = keys["Password"];
try
{
var certificate = new X509Certificate2(_p12Path, p12Password);
Console.WriteLine($"Certificate Subject: {certificate.Subject}");
Console.WriteLine($"Certificate Issuer: {certificate.Issuer}");
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
using var client = new HttpClient(handler)
{
BaseAddress = new Uri(_uri),
Timeout = TimeSpan.FromSeconds(60)
};
var request = new HttpRequestMessage(HttpMethod.Post, $"/3/device/{deviceToken}")
{
Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"),
Version = new Version(2, 0) // HTTP/2 사용
};
// 필수 헤더 추가
request.Headers.Add("apns-topic", _apnsTopic);
request.Headers.Add("apns-push-type", "alert");
Console.WriteLine($"Send -> Payload: {jsonPayload}");
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return new APIResult
{
Success = true
};
}
else
{
var errorContent = await response.Content.ReadAsStringAsync();
return new APIResult
{
Success = false,
Code = "003",
Message = $"APN 통신 실패 = {response.StatusCode} : {errorContent}"
};
}
}
catch (HttpRequestException httpEx)
{
Console.WriteLine($"HttpRequestException: {httpEx.Message}");
return new APIResult
{
Success = false,
Code = "003",
Message = $"통신 실패 : {httpEx.Message}"
};
}
catch (CryptographicException ex)
{
return new APIResult
{
Success = false,
Code = "999",
Message = $"오류 발생 : {ex.Message}"
};
}
catch (Exception ex)
{
return new APIResult
{
Success = false,
Code = "999",
Message = $"오류 발생 : {ex.Message}"
};
}
}
}

View File

@ -0,0 +1,136 @@
using AcaMate.Common.Data;
using AcaMate.Common.Token;
using AcaMate.Common.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
namespace AcaMate.V1.Services;
public interface IRepositoryService
{
Task<bool> SaveData<T, K>(T entity, Expression<Func<T, K>> key) where T : class;
Task<ValidateToken> ValidateToken(string token, string refresh);
}
public class RepositoryService: IRepositoryService
{
private readonly AppDbContext _dbContext;
private readonly ILogger<RepositoryService> _logger;
private readonly JwtTokenService _jwtTokenService;
public RepositoryService(AppDbContext dbContext, ILogger<RepositoryService> logger, JwtTokenService jwtTokenService)
{
_dbContext = dbContext;
_logger = logger;
_jwtTokenService = jwtTokenService;
}
public async Task<bool> SaveData<T, K> (T entity, Expression<Func<T, K>> key) where T : class
{
try
{
var value = key.Compile()(entity);
// x 라 함은 Expression 으로 생성되는 트리에서 T 타입으의 매개변수를 지칭함
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var dbSet = _dbContext.Set<T>();
var entityData = await dbSet.FirstOrDefaultAsync(predicate);
if (entityData != null)
{
_logger.LogInformation($"[{typeof(T)}] 해당 PK 존재 [{value}]: 계속");
var entry = _dbContext.Entry(entityData);
entry.CurrentValues.SetValues(entity);
if (entry.Properties.Any(p => p.IsModified))
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 존재: 계속");
}
else
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 없음: 종료");
return true;
}
}
else
{
_logger.LogInformation($"[{typeof(T)}] 처음등록");
dbSet.Add(entity);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"[{typeof(T)}] DB 저장 완료: 종료");
return true;
}
catch (Exception ex)
{
_logger.LogInformation($"[{typeof(T)}] 알 수 없는 오류: 종료 {ex}");
return false;
}
}
//토큰 태울때는 인코딩 된 걸로 태워야지 원본꺼 태우면 데이터에 손상옵니다.
public async Task<ValidateToken> ValidateToken(string token, string refresh)
{
var principalToken = _jwtTokenService.ValidateToken(token);
if (principalToken != null)
{
var uid = principalToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
_logger.LogInformation($"토큰 변환 - {uid}");
return new ValidateToken
{
token = token,
refresh = refresh,
uid = uid
};
}
else
{
_logger.LogInformation("엑세스 토큰 만료");
var refreshToken = await _dbContext.RefreshTokens
.FirstOrDefaultAsync(t => t.refresh_token == refresh);
if (refreshToken == null)
{
throw new SecurityTokenException("리프레시 토큰도 잘못되었음");
}
var uid = refreshToken.uid;
if (refreshToken.expire_date > DateTime.Now)
{
_logger.LogInformation($"리프레시 : {uid}");
var access = _jwtTokenService.GenerateJwtToken(uid);
return new ValidateToken
{
token = access,
refresh = refreshToken.refresh_token,
uid = uid
};
}
else
{
refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
_logger.LogInformation("리프레시 토큰 만료");
await SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
return new ValidateToken
{
token = token,
refresh = refreshToken.refresh_token,
uid = uid
};
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace AcaMate.V1.Services;
public class UserService
{
// priva
}

View File

@ -4,4 +4,13 @@
### Skill
- .NET Web API
### IDE
- JetBrains Rider
- JetBrains Rider
### 추가 패키지
| No. | Name | Version | Description |
|:---:|:-------------------------------------------:|:-------:|:---------------------------|
| 1 | Microsoft.AspNetCore.OpenApi | 8.0.10 | OpenAPI 를 지원하기 위해 사용 |
| 2 | Microsoft.EntityFrameworkCore | 8.0.10 | 데이터베이스 작업을 간편하게 수행하기 위해 사용 |
| 3 | Pomelo.EntityFrameworkCore.MySql | 8.0.2 | MariaDB 연결 |
| 4 |Microsoft.AspNetCore.Authentication.JwtBearer| 8.0.10 | |
| 5 |Dapper|2.1.35|SQL 직접 작성|

92
SwaggerConfigure.cs Normal file
View File

@ -0,0 +1,92 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.SwaggerGen;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CustomOperationAttribute : Attribute
{
public string Summary { get; }
public string Description { get; }
public string[] Tags { get; }
public CustomOperationAttribute(string summary, string description, params string[] tags)
{
Summary = summary;
Description = description;
Tags = tags;
}
}
public class CustomSwaggerOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var customSwaggerAttribute = context.MethodInfo.GetCustomAttributes(typeof(CustomOperationAttribute), false)
.FirstOrDefault() as CustomOperationAttribute;
if (customSwaggerAttribute != null)
{
operation.Summary = customSwaggerAttribute.Summary;
operation.Description = customSwaggerAttribute.Description;
operation.Tags = customSwaggerAttribute.Tags
.Select(tag => new OpenApiTag { Name = tag })
.ToList();
}
}
}
public static class SwaggerConfigure
{
private static OpenApiInfo DocName(string title, string version)
{
return new OpenApiInfo
{
Title = title,
Version = version
};
}
public static void AddCustomSwagger(this IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
options.EnableAnnotations();
options.OperationFilter<CustomSwaggerOperationFilter>();
options.SwaggerDoc("전체", DocName("AcaMate 전체 API","1.0.0"));
options.SwaggerDoc("공통",DocName("공통 API", "1.0.0"));
options.SwaggerDoc("사업자 정보", DocName("사업자 정보 API", "1.0.0"));
options.SwaggerDoc("사용자", DocName("사용자 API", "1.0.0"));
options.DocInclusionPredicate((docName, apiDesc) =>
{
if (docName == "전체") return true; // 전체 문서에 모든 API 포함
if (docName == "공통" && apiDesc.GroupName == "공통") return true;
if (docName == "사업자 정보" && apiDesc.GroupName == "사업자 정보") return true;
if (docName == "사용자" && apiDesc.GroupName == "사용자") return true;
return false;
});
options.TagActionsBy(apiDesc => new[] { apiDesc.GroupName ?? "기타" });
// options.TagActionsBy(apiDesc => apiDesc.ActionDescriptor.EndpointMetadata
// .OfType<SwaggerOperationAttribute>()
// .FirstOrDefault()?.Tags ?? new[] { "기타" });
});
}
public static void UseCustomSwaggerUI(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/전체/swagger.json", "전체 API");
options.SwaggerEndpoint("/swagger/공통/swagger.json", "공통 API");
options.SwaggerEndpoint("/swagger/사용자/swagger.json", "사용자 API");
options.SwaggerEndpoint("/swagger/사업자 정보/swagger.json", "사업자 정보 API");
});
}
}

View File

@ -1,7 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},